strelka 0.0.1pre4 → 0.0.1.pre129
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.
- data/History.rdoc +1 -1
- data/IDEAS.rdoc +62 -0
- data/Manifest.txt +38 -7
- data/README.rdoc +124 -5
- data/Rakefile +22 -6
- data/bin/leash +102 -157
- data/contrib/hoetemplate/.autotest.erb +23 -0
- data/contrib/hoetemplate/History.rdoc.erb +4 -0
- data/contrib/hoetemplate/Manifest.txt.erb +8 -0
- data/contrib/hoetemplate/README.rdoc.erb +17 -0
- data/contrib/hoetemplate/Rakefile.erb +24 -0
- data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
- data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
- data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
- data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
- data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
- data/data/strelka/apps/hello-world +30 -0
- data/lib/strelka/app/defaultrouter.rb +49 -30
- data/lib/strelka/app/errors.rb +121 -0
- data/lib/strelka/app/exclusiverouter.rb +40 -0
- data/lib/strelka/app/filters.rb +18 -7
- data/lib/strelka/app/negotiation.rb +122 -0
- data/lib/strelka/app/parameters.rb +171 -14
- data/lib/strelka/app/paramvalidator.rb +751 -0
- data/lib/strelka/app/plugins.rb +66 -46
- data/lib/strelka/app/restresources.rb +499 -0
- data/lib/strelka/app/router.rb +73 -0
- data/lib/strelka/app/routing.rb +140 -18
- data/lib/strelka/app/templating.rb +12 -3
- data/lib/strelka/app.rb +174 -24
- data/lib/strelka/constants.rb +0 -20
- data/lib/strelka/exceptions.rb +29 -0
- data/lib/strelka/httprequest/acceptparams.rb +377 -0
- data/lib/strelka/httprequest/negotiation.rb +257 -0
- data/lib/strelka/httprequest.rb +155 -7
- data/lib/strelka/httpresponse/negotiation.rb +579 -0
- data/lib/strelka/httpresponse.rb +140 -0
- data/lib/strelka/logging.rb +4 -1
- data/lib/strelka/mixins.rb +53 -0
- data/lib/strelka.rb +22 -1
- data/spec/data/error.tmpl +1 -0
- data/spec/lib/constants.rb +0 -1
- data/spec/lib/helpers.rb +21 -0
- data/spec/strelka/app/defaultrouter_spec.rb +41 -35
- data/spec/strelka/app/errors_spec.rb +212 -0
- data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
- data/spec/strelka/app/filters_spec.rb +196 -0
- data/spec/strelka/app/negotiation_spec.rb +73 -0
- data/spec/strelka/app/parameters_spec.rb +149 -0
- data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
- data/spec/strelka/app/plugins_spec.rb +26 -19
- data/spec/strelka/app/restresources_spec.rb +393 -0
- data/spec/strelka/app/router_spec.rb +63 -0
- data/spec/strelka/app/routing_spec.rb +183 -9
- data/spec/strelka/app/templating_spec.rb +1 -2
- data/spec/strelka/app_spec.rb +265 -32
- data/spec/strelka/exceptions_spec.rb +53 -0
- data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
- data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
- data/spec/strelka/httprequest_spec.rb +204 -14
- data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
- data/spec/strelka/httpresponse_spec.rb +114 -0
- data/spec/strelka/mixins_spec.rb +99 -0
- data.tar.gz.sig +1 -0
- metadata +175 -79
- metadata.gz.sig +2 -0
- data/IDEAS.textile +0 -174
- data/data/strelka/apps/strelka-admin +0 -65
- data/data/strelka/apps/strelka-setup +0 -26
- data/data/strelka/bootstrap-config.rb +0 -34
- data/data/strelka/templates/admin/console.tmpl +0 -21
- data/data/strelka/templates/layout.tmpl +0 -30
- data/lib/strelka/process.rb +0 -19
@@ -0,0 +1,579 @@
|
|
1
|
+
#!usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'yaml'
|
5
|
+
require 'yajl'
|
6
|
+
|
7
|
+
require 'strelka/constants'
|
8
|
+
require 'strelka/exceptions'
|
9
|
+
require 'strelka/httpresponse' unless defined?( Strelka::HTTPResponse )
|
10
|
+
|
11
|
+
|
12
|
+
# The mixin that adds methods to Strelka::HTTPResponse for content-negotiation.
|
13
|
+
#
|
14
|
+
# response = request.response
|
15
|
+
# response.for( 'text/html' ) {...}
|
16
|
+
# response.for( :json ) {...}
|
17
|
+
# response.for_encoding( :en ) {...}
|
18
|
+
# response.for_language( :en ) {...}
|
19
|
+
#
|
20
|
+
# If the response was created from a request, it also knows whether or not it
|
21
|
+
# is acceptable according to its request's `Accept*` headers.
|
22
|
+
#
|
23
|
+
module Strelka::HTTPResponse::Negotiation
|
24
|
+
include Strelka::Constants
|
25
|
+
|
26
|
+
# TODO: Perhaps replace this with something like this:
|
27
|
+
# Mongrel2::Config::Mimetype.to_hash( :extension => :mimetype )
|
28
|
+
BUILTIN_MIMETYPES = {
|
29
|
+
:html => 'text/html',
|
30
|
+
:text => 'text/plain',
|
31
|
+
|
32
|
+
:yaml => 'application/x-yaml',
|
33
|
+
:json => 'application/json',
|
34
|
+
|
35
|
+
:jpeg => 'image/jpeg',
|
36
|
+
:png => 'image/png',
|
37
|
+
:gif => 'image/gif',
|
38
|
+
|
39
|
+
:rdf => 'application/rdf+xml',
|
40
|
+
:rss => 'application/rss+xml',
|
41
|
+
:atom => 'application/atom+xml',
|
42
|
+
}
|
43
|
+
|
44
|
+
# A collection of stringifier callbacks, keyed by mimetype. If an object other
|
45
|
+
# than a String is returned by a content callback, and an entry for the callback's
|
46
|
+
# mimetype exists in this Hash, it will be #call()ed to stringify the object.
|
47
|
+
STRINGIFIERS = {
|
48
|
+
'application/x-yaml' => YAML.method( :dump ),
|
49
|
+
'application/json' => Yajl.method( :dump ),
|
50
|
+
}
|
51
|
+
|
52
|
+
|
53
|
+
### Add some instance variables for negotiation.
|
54
|
+
def initialize( * )
|
55
|
+
@mediatype_callbacks = {}
|
56
|
+
@language_callbacks = {}
|
57
|
+
@encoding_callbacks = {}
|
58
|
+
|
59
|
+
@vary_fields = Set.new
|
60
|
+
|
61
|
+
super
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
######
|
66
|
+
public
|
67
|
+
######
|
68
|
+
|
69
|
+
# The Hash of mediatype alternative callbacks for content negotiation,
|
70
|
+
# keyed by mimetype.
|
71
|
+
attr_reader :mediatype_callbacks
|
72
|
+
|
73
|
+
# The Hash of language alternative callbacks for content negotiation,
|
74
|
+
# keyed by language tag String.
|
75
|
+
attr_reader :language_callbacks
|
76
|
+
|
77
|
+
# The Hash of document coding alternative callbacks for content
|
78
|
+
# negotiation, keyed by coding name.
|
79
|
+
attr_reader :encoding_callbacks
|
80
|
+
|
81
|
+
# A Set of header fields to add to the 'Vary:' response header.
|
82
|
+
attr_reader :vary_fields
|
83
|
+
|
84
|
+
|
85
|
+
### Overridden to reset content-negotiation callbacks, too.
|
86
|
+
def reset
|
87
|
+
super
|
88
|
+
|
89
|
+
@mediatype_callbacks.clear
|
90
|
+
@language_callbacks.clear
|
91
|
+
@encoding_callbacks.clear
|
92
|
+
|
93
|
+
# Not clearing the Vary: header for now, as it's useful in a 406 to
|
94
|
+
# determine what accept-* headers can be modified to get an acceptable
|
95
|
+
# response
|
96
|
+
# @vary_fields.clear
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
### Overridden to add a Vary: header to outgoing headers if the response has
|
101
|
+
### any #vary_fields.
|
102
|
+
def normalized_headers
|
103
|
+
headers = super
|
104
|
+
|
105
|
+
unless self.vary_fields.empty?
|
106
|
+
self.log.debug "Adding Vary header for %p" % [ self.vary_fields ]
|
107
|
+
headers.vary = self.vary_fields.to_a.join( ', ' )
|
108
|
+
end
|
109
|
+
|
110
|
+
return headers
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
### Stringify the response -- overridden to use the negotiated body.
|
115
|
+
def to_s
|
116
|
+
return [
|
117
|
+
self.status_line,
|
118
|
+
self.header_data,
|
119
|
+
self.negotiated_body
|
120
|
+
].join( "\r\n" )
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
### Transform the entity body if it doesn't meet the criteria
|
125
|
+
def negotiated_body
|
126
|
+
return '' if self.bodiless?
|
127
|
+
|
128
|
+
self.negotiate
|
129
|
+
return self.body
|
130
|
+
end
|
131
|
+
|
132
|
+
|
133
|
+
### Check for any negotiation that should happen and apply the necessary
|
134
|
+
### transforms if they're available.
|
135
|
+
def negotiate
|
136
|
+
return if !self.request
|
137
|
+
self.transform_content_type
|
138
|
+
self.transform_language
|
139
|
+
self.transform_charset
|
140
|
+
self.transform_encoding
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
#
|
145
|
+
# :section: Acceptance Predicates
|
146
|
+
#
|
147
|
+
|
148
|
+
### Return true if the receiver satisfies all of its originating request's
|
149
|
+
### Accept* headers, or it's a bodiless response.
|
150
|
+
def acceptable?
|
151
|
+
# self.negotiate
|
152
|
+
return self.bodiless? ||
|
153
|
+
( self.acceptable_content_type? &&
|
154
|
+
self.acceptable_charset? &&
|
155
|
+
self.acceptable_language? &&
|
156
|
+
self.acceptable_encoding? )
|
157
|
+
end
|
158
|
+
alias_method :is_acceptable?, :acceptable?
|
159
|
+
|
160
|
+
|
161
|
+
### Returns true if the content-type of the response is set to a
|
162
|
+
### mediatype that was designated as acceptable by the originating
|
163
|
+
### request, or if there was no originating request.
|
164
|
+
def acceptable_content_type?
|
165
|
+
req = self.request or return true
|
166
|
+
answer = req.accepts?( self.content_type )
|
167
|
+
self.log.warn "Content-type %p NOT acceptable: %p" %
|
168
|
+
[ self.content_type, req.accepted_mediatypes ] unless answer
|
169
|
+
|
170
|
+
return answer
|
171
|
+
end
|
172
|
+
alias_method :has_acceptable_content_type?, :acceptable_content_type?
|
173
|
+
|
174
|
+
|
175
|
+
### Returns true if the receiver's #charset is set to a value that was
|
176
|
+
### designated as acceptable by the originating request, or if there
|
177
|
+
### was no originating request.
|
178
|
+
def acceptable_charset?
|
179
|
+
req = self.request or return true
|
180
|
+
charset = self.find_header_charset
|
181
|
+
|
182
|
+
# Types other than text are binary, and so aren't subject to charset
|
183
|
+
# acceptability.
|
184
|
+
# For 'text/' subtypes:
|
185
|
+
# When no explicit charset parameter is provided by the sender, media
|
186
|
+
# subtypes of the "text" type are defined to have a default charset
|
187
|
+
# value of "ISO-8859-1" when received via HTTP. [RFC2616 3.7.1]
|
188
|
+
if charset == Encoding::ASCII_8BIT
|
189
|
+
return true unless self.content_type.start_with?( 'text/' )
|
190
|
+
charset = Encoding::ISO8859_1
|
191
|
+
end
|
192
|
+
|
193
|
+
answer = req.accepts_charset?( charset )
|
194
|
+
self.log.warn "Content-charset %p NOT acceptable: %p" %
|
195
|
+
[ self.find_header_charset, req.accepted_charsets ] unless answer
|
196
|
+
|
197
|
+
return answer
|
198
|
+
end
|
199
|
+
alias_method :has_acceptable_charset?, :acceptable_charset?
|
200
|
+
|
201
|
+
|
202
|
+
### Returns true if at least one of the receiver's #languages is set
|
203
|
+
### to a value that was designated as acceptable by the originating
|
204
|
+
### request, if there was no originating request, or if no #languages
|
205
|
+
### have been set for a non-empty entity body.
|
206
|
+
def acceptable_language?
|
207
|
+
req = self.request or return true
|
208
|
+
|
209
|
+
# Lack of an accept-language field means all languages are accepted
|
210
|
+
return true if req.accepted_languages.empty?
|
211
|
+
|
212
|
+
# If no language is given for an existing entity body, there's no way
|
213
|
+
# to know whether or not there's a better alternative
|
214
|
+
return true if self.languages.empty?
|
215
|
+
|
216
|
+
# If any of the languages present for the body are accepted, the
|
217
|
+
# request is acceptable. Or at least that's what I got out of
|
218
|
+
# reading RFC2616, Section 14.4.
|
219
|
+
answer = self.languages.any? {|lang| req.accepts_language?(lang) }
|
220
|
+
self.log.warn "Content-language %p NOT acceptable: %s" %
|
221
|
+
[ self.languages, req.accepted_languages ] unless answer
|
222
|
+
|
223
|
+
return answer
|
224
|
+
end
|
225
|
+
alias_method :has_acceptable_language?, :acceptable_language?
|
226
|
+
|
227
|
+
|
228
|
+
### Returns true if all of the receiver's #encodings were designated
|
229
|
+
### as acceptable by the originating request, if there was no originating
|
230
|
+
### request, or if no #encodings have been set.
|
231
|
+
def acceptable_encoding?
|
232
|
+
req = self.request or return true
|
233
|
+
|
234
|
+
encs = self.encodings.dup
|
235
|
+
encs << 'identity' if encs.empty?
|
236
|
+
|
237
|
+
answer = encs.all? {|enc| req.accepts_encoding?(enc) }
|
238
|
+
self.log.warn "Content-encoding %p NOT acceptable: %s" %
|
239
|
+
[ encs, req.accepted_encodings ] unless answer
|
240
|
+
|
241
|
+
return answer
|
242
|
+
end
|
243
|
+
alias_method :has_acceptable_encoding?, :acceptable_encoding?
|
244
|
+
|
245
|
+
|
246
|
+
#
|
247
|
+
# :section: Content-type Callbacks
|
248
|
+
#
|
249
|
+
|
250
|
+
### Register a callback that will be called during transparent content
|
251
|
+
### negotiation for the entity body if one or more of the specified
|
252
|
+
### +mediatypes+ is among the requested alternatives. The +mediatypes+
|
253
|
+
### can be either mimetype Strings or Symbols that correspond to keys
|
254
|
+
### in the BUILTIN_MIMETYPES hash. The +callback+ will be called with
|
255
|
+
### the desired mimetype, and should return the new value for the entity
|
256
|
+
### body if it successfully transformed the body, or a false value if
|
257
|
+
### the next alternative should be tried instead.
|
258
|
+
### If successful, the response's body will be set to the new value,
|
259
|
+
### its content_type set to the new mimetype, and its status changed
|
260
|
+
### to HTTP::OK.
|
261
|
+
def for( *mediatypes, &callback )
|
262
|
+
mediatypes.each do |mimetype|
|
263
|
+
mimetype = BUILTIN_MIMETYPES[ mimetype ] if mimetype.is_a?( Symbol )
|
264
|
+
self.mediatype_callbacks[ mimetype ] = callback
|
265
|
+
end
|
266
|
+
|
267
|
+
# Include the 'Accept:' header in the 'Vary:' header
|
268
|
+
self.vary_fields.add( 'accept' )
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
### Returns Strelka::HTTPRequest::MediaType objects for mediatypes that have
|
273
|
+
### a higher qvalue than the current response's entity body (if any).
|
274
|
+
def better_mediatypes
|
275
|
+
req = self.request or return []
|
276
|
+
return [] unless req.headers.accept
|
277
|
+
|
278
|
+
current_qvalue = 0.0
|
279
|
+
mediatypes = req.accepted_mediatypes.sort
|
280
|
+
|
281
|
+
# If the current mediatype exists in the Accept: header, reset the current qvalue
|
282
|
+
# to whatever its qvalue is
|
283
|
+
if self.content_type
|
284
|
+
mediatype = mediatypes.find {|mt| mt =~ self.content_type }
|
285
|
+
current_qvalue = mediatype.qvalue if mediatype
|
286
|
+
end
|
287
|
+
|
288
|
+
self.log.debug "Looking for better mediatypes than %p (%0.2f)" %
|
289
|
+
[ self.content_type, current_qvalue ]
|
290
|
+
|
291
|
+
return mediatypes.find_all do |mt|
|
292
|
+
mt.qvalue > current_qvalue
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
### Iterate over the originating request's acceptable content types in
|
298
|
+
### qvalue+listed order, looking for a content negotiation callback for
|
299
|
+
### each mediatype. If any are found, they are tried in declared order
|
300
|
+
### until one returns a true-ish value, which becomes the new entity
|
301
|
+
### body. If the body object is not a String,
|
302
|
+
def transform_content_type
|
303
|
+
return if self.mediatype_callbacks.empty?
|
304
|
+
|
305
|
+
self.log.debug "Applying content-type transforms (if any)"
|
306
|
+
self.better_mediatypes.each do |mediatype|
|
307
|
+
callbacks = self.mediatype_callbacks.find_all do |mimetype, _|
|
308
|
+
mediatype =~ mimetype
|
309
|
+
end
|
310
|
+
|
311
|
+
if callbacks.empty?
|
312
|
+
self.log.debug " no transforms for %s" % [ mediatype ]
|
313
|
+
else
|
314
|
+
callbacks.each do |mimetype, callback|
|
315
|
+
return if self.try_content_type_callback( mimetype, callback )
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
### Attempt to apply the +callback+ for the specified +mediatype+ to the entity
|
323
|
+
### body, making the necessary changes to the request and returning +true+ if
|
324
|
+
### the callback returns a new entity body, or returning a false value if it doesn't.
|
325
|
+
def try_content_type_callback( mimetype, callback )
|
326
|
+
self.log.debug " trying content-type callback %p (%s)" % [ callback, mimetype ]
|
327
|
+
|
328
|
+
new_body = callback.call( mimetype ) or return false
|
329
|
+
|
330
|
+
self.log.debug " successfully transformed! Setting up response."
|
331
|
+
new_body = STRINGIFIERS[ mimetype ].call( new_body ) if
|
332
|
+
STRINGIFIERS.key?( mimetype ) && !new_body.is_a?( String )
|
333
|
+
|
334
|
+
self.body = new_body
|
335
|
+
self.content_type = mimetype
|
336
|
+
self.status ||= HTTP::OK
|
337
|
+
|
338
|
+
return true
|
339
|
+
end
|
340
|
+
|
341
|
+
|
342
|
+
#
|
343
|
+
# :section: Language negotiation callbacks
|
344
|
+
#
|
345
|
+
|
346
|
+
### Register a callback that will be called during transparent content
|
347
|
+
### negotiation for the entity body if one or more of the specified
|
348
|
+
### +language_tags+ is among the requested alternatives. The +language_tags+
|
349
|
+
### are Strings in the form described by RFC2616, section 3.10. The
|
350
|
+
### +callback+ will be called with the desired language code, and should
|
351
|
+
### return the new value for the entity body if it has value for the
|
352
|
+
### body, or a false value if the next alternative should be tried
|
353
|
+
### instead. If successful, the response's body will be set to the new
|
354
|
+
### value, and its status changed to HTTP::OK.
|
355
|
+
def for_language( *language_tags, &callback )
|
356
|
+
language_tags.flatten.each do |lang|
|
357
|
+
self.language_callbacks[ lang.to_sym ] = callback
|
358
|
+
end
|
359
|
+
|
360
|
+
# Include the 'Accept-Language:' header in the 'Vary:' header
|
361
|
+
self.vary_fields.add( 'accept-language' )
|
362
|
+
end
|
363
|
+
|
364
|
+
|
365
|
+
### Returns Strelka::HTTPRequest::Language objects for natural languages that have
|
366
|
+
### a higher qvalue than the current response's entity body (if any).
|
367
|
+
def better_languages
|
368
|
+
req = self.request or return []
|
369
|
+
|
370
|
+
current_qvalue = 0.0
|
371
|
+
accepted_languages = req.accepted_languages.sort
|
372
|
+
|
373
|
+
# If any of the current languages exists in the Accept-Language: header, reset
|
374
|
+
# the current qvalue to the highest one among them
|
375
|
+
unless self.languages.empty?
|
376
|
+
current_qvalue = self.languages.reduce( current_qvalue ) do |qval, lang|
|
377
|
+
accepted_lang = accepted_languages.find {|alang| alang =~ lang } or
|
378
|
+
next qval
|
379
|
+
qval > accepted_lang.qvalue ? qval : accepted_lang.qvalue
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
self.log.debug "Looking for better languages than %p (%0.2f)" %
|
384
|
+
[ self.languages.join(', '), current_qvalue ]
|
385
|
+
|
386
|
+
return accepted_languages.find_all do |lang|
|
387
|
+
lang.qvalue > current_qvalue
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
|
392
|
+
### If there are any languages that have a higher qvalue than the one/s in #languages,
|
393
|
+
### look for a negotiation callback that provides that language. If any are found, they
|
394
|
+
### are tried in declared order until one returns a true-ish value, which becomes the new
|
395
|
+
### entity body.
|
396
|
+
def transform_language
|
397
|
+
return if self.language_callbacks.empty?
|
398
|
+
|
399
|
+
self.log.debug "Looking for language transformations"
|
400
|
+
self.better_languages.uniq.each do |lang|
|
401
|
+
callback = langcode = nil
|
402
|
+
|
403
|
+
if lang.primary_tag
|
404
|
+
langcode = lang.language_range
|
405
|
+
callback = self.language_callbacks[ lang.primary_tag.to_sym ]
|
406
|
+
else
|
407
|
+
langcode, callback = self.language_callbacks.first
|
408
|
+
end
|
409
|
+
|
410
|
+
next unless callback
|
411
|
+
|
412
|
+
self.log.debug " found a callback for %s: %p" % [ langcode, callback ]
|
413
|
+
if (( new_body = callback.call(langcode) ))
|
414
|
+
self.body = new_body
|
415
|
+
self.languages.replace([ langcode.to_s ])
|
416
|
+
self.log.debug " success."
|
417
|
+
break
|
418
|
+
end
|
419
|
+
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
|
424
|
+
#
|
425
|
+
# :section: Charset negotiation callbacks
|
426
|
+
#
|
427
|
+
|
428
|
+
### Returns Strelka::HTTPRequest::Charset objects for accepted character sets that have
|
429
|
+
### a higher qvalue than the one used by the current response.
|
430
|
+
def better_charsets
|
431
|
+
req = self.request or return []
|
432
|
+
return [] unless self.content_type &&
|
433
|
+
self.content_type.start_with?( 'text/', 'application/' )
|
434
|
+
return [] unless req.headers.accept_charset
|
435
|
+
|
436
|
+
current_qvalue = 0.0
|
437
|
+
charsets = req.accepted_charsets.sort
|
438
|
+
current_charset = self.find_header_charset
|
439
|
+
|
440
|
+
# If the current charset exists in the Accept-Charset: header, reset the current qvalue
|
441
|
+
# to whatever its qvalue is
|
442
|
+
if current_charset != Encoding::ASCII_8BIT
|
443
|
+
charset = charsets.find {|mt| mt =~ current_charset }
|
444
|
+
current_qvalue = charset.qvalue if charset
|
445
|
+
end
|
446
|
+
|
447
|
+
self.log.debug "Looking for better charsets than %p (%0.2f)" %
|
448
|
+
[ current_charset, current_qvalue ]
|
449
|
+
|
450
|
+
return charsets.sort.find_all do |cs|
|
451
|
+
cs.qvalue > current_qvalue
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
### Iterate over the originating request's acceptable charsets in
|
457
|
+
### qvalue+listed order, attempting to transcode the current entity body
|
458
|
+
### if it
|
459
|
+
def transform_charset
|
460
|
+
self.log.debug "Looking for charset transformations."
|
461
|
+
self.better_charsets.each do |charset|
|
462
|
+
self.log.debug " trying to transcode to: %s" % [ charset ]
|
463
|
+
|
464
|
+
if self.body.respond_to?( :encode )
|
465
|
+
self.log.debug " body is a string; trying direct transcoding"
|
466
|
+
if self.transcode_body_string( charset )
|
467
|
+
self.log.debug " success; body is now %p" % [ self.body.encoding ]
|
468
|
+
self.vary_fields.add( 'accept-charset' )
|
469
|
+
break
|
470
|
+
end
|
471
|
+
|
472
|
+
# Can change the external_encoding if it's a File that has a #path
|
473
|
+
elsif self.body.respond_to?( :external_encoding )
|
474
|
+
raise NotImplementedError,
|
475
|
+
"Support for transcoding %p objects isn't done." % [ self.body.class ]
|
476
|
+
else
|
477
|
+
self.log.warn "Don't know how to transcode a %p" % [ self.body.class ]
|
478
|
+
end
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
|
483
|
+
### Try to transcode the entity body String to one of the specified +charsets+. Returns
|
484
|
+
### the succesful Encoding object if transcoding succeeded, or +nil+ if transcoding
|
485
|
+
### failed.
|
486
|
+
def transcode_body_string( charset )
|
487
|
+
unless enc = charset.encoding_object
|
488
|
+
self.log.warn " unsupported charset: %s" % [ charset ]
|
489
|
+
return false
|
490
|
+
end
|
491
|
+
|
492
|
+
succeeded = false
|
493
|
+
begin
|
494
|
+
succeeded = self.body.encode!( enc )
|
495
|
+
rescue Encoding::UndefinedConversionError => err
|
496
|
+
self.log.error "%p while transcoding: %s" % [ err.class, err.message ]
|
497
|
+
end
|
498
|
+
|
499
|
+
return succeeded
|
500
|
+
end
|
501
|
+
|
502
|
+
|
503
|
+
#
|
504
|
+
# :section: Content-coding negotiation callbacks
|
505
|
+
#
|
506
|
+
|
507
|
+
### Register a callback that will be called during transparent content
|
508
|
+
### negotiation for the entity body if one or more of the specified
|
509
|
+
### +codings+ is among the requested alternatives. The +codings+
|
510
|
+
### are Strings in the form described by RFC2616, section 3.5. The
|
511
|
+
### +callback+ will be called with the coding name, and should
|
512
|
+
### return the new value for the entity body if it has transformed the
|
513
|
+
### bod. If successful, the response's body will be set to the new
|
514
|
+
### value, and the coding name added to the appropriate headers.
|
515
|
+
def for_encoding( *codings, &callback )
|
516
|
+
codings.each do |coding|
|
517
|
+
self.encoding_callbacks[ coding ] = callback
|
518
|
+
end
|
519
|
+
|
520
|
+
# Include the 'Accept-Encoding:' header in the 'Vary:' header
|
521
|
+
self.vary_fields.add( 'accept-encoding' )
|
522
|
+
end
|
523
|
+
|
524
|
+
|
525
|
+
### Returns Strelka::HTTPRequest::Encoding objects for accepted encodings that have
|
526
|
+
### a higher qvalue than the one used by the current response.
|
527
|
+
def better_encoding
|
528
|
+
req = self.request or return []
|
529
|
+
return [] unless req.headers.accept_encoding
|
530
|
+
|
531
|
+
current_qvalue = 0.0
|
532
|
+
encodings = req.accepted_encodings.sort
|
533
|
+
current_encodings = self.encodings.dup
|
534
|
+
current_encodings.unshift( 'identity' )
|
535
|
+
|
536
|
+
# Find the highest qvalue of the encodings that have been applied already
|
537
|
+
current_qvalue = current_encodings.inject( current_qvalue ) do |qval, current_enc|
|
538
|
+
qenc = encodings.find {|enc| enc =~ current_enc } or next qval
|
539
|
+
qenc.qvalue > qval ? qenc.qvalue : qval
|
540
|
+
end
|
541
|
+
|
542
|
+
self.log.debug "Looking for better encodings than %p (%0.2f)" %
|
543
|
+
[ current_encodings, current_qvalue ]
|
544
|
+
|
545
|
+
return encodings.find_all do |enc|
|
546
|
+
self.log.debug " %s (%0.2f) > %0.2f?" % [ enc, enc.qvalue, current_qvalue ]
|
547
|
+
enc.qvalue > current_qvalue
|
548
|
+
end
|
549
|
+
end
|
550
|
+
|
551
|
+
|
552
|
+
### Iterate over the originating request's acceptable encodings and apply
|
553
|
+
### each one in the order they were requested if they're available.
|
554
|
+
def transform_encoding
|
555
|
+
return if self.encoding_callbacks.empty?
|
556
|
+
|
557
|
+
self.log.debug "Looking for acceptable content codings"
|
558
|
+
self.better_encoding.each do |enc|
|
559
|
+
self.log.debug " looking for a callback for %p" % [ enc ]
|
560
|
+
|
561
|
+
if (( callback = self.encoding_callbacks[enc.content_coding.to_sym] ))
|
562
|
+
self.log.debug " trying callback %p for %p" %
|
563
|
+
[ callback, enc ]
|
564
|
+
if (( new_body = callback.call(enc.content_coding) ))
|
565
|
+
self.log.debug " callback succeeded"
|
566
|
+
self.body = new_body
|
567
|
+
self.encodings << enc.content_coding
|
568
|
+
break
|
569
|
+
end
|
570
|
+
elsif enc.content_coding == 'identity' && enc.qvalue.nonzero?
|
571
|
+
self.log.debug " identity coding, no callback"
|
572
|
+
break
|
573
|
+
end
|
574
|
+
end
|
575
|
+
end
|
576
|
+
|
577
|
+
|
578
|
+
end # module Strelka::HTTPResponse::Negotiation
|
579
|
+
|
@@ -0,0 +1,140 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'mongrel2/httpresponse'
|
4
|
+
require 'strelka' unless defined?( Strelka )
|
5
|
+
|
6
|
+
# An HTTP response class.
|
7
|
+
class Strelka::HTTPResponse < Mongrel2::HTTPResponse
|
8
|
+
include Strelka::Loggable,
|
9
|
+
Strelka::Constants
|
10
|
+
|
11
|
+
|
12
|
+
# Pattern for matching a 'charset' parameter in a media-type string, such as the
|
13
|
+
# Content-type header
|
14
|
+
CONTENT_TYPE_CHARSET_RE = /;\s*charset=(?<charset>\S+)\s*/i
|
15
|
+
|
16
|
+
|
17
|
+
### Add some instance variables to new HTTPResponses.
|
18
|
+
def initialize( * ) # :notnew:
|
19
|
+
@charset = nil
|
20
|
+
@languages = []
|
21
|
+
@encodings = []
|
22
|
+
|
23
|
+
super
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
######
|
28
|
+
public
|
29
|
+
######
|
30
|
+
|
31
|
+
# Overridden charset of the response's entity body, as either an
|
32
|
+
# Encoding or a String. This will be appended to the Content-type
|
33
|
+
# header when the response is sent to the client, replacing any charset
|
34
|
+
# setting in the Content-type header already. Defaults to nil, which
|
35
|
+
# will cause the encoding of the entity body object to be used instead
|
36
|
+
# unless there's already one present in the Content-type. In any
|
37
|
+
# case, if the encoding is Encoding::ASCII_8BIT, no charset will be
|
38
|
+
# appended to the content-type header.
|
39
|
+
attr_accessor :charset
|
40
|
+
|
41
|
+
# An Array of any encodings that have been applied to the response's
|
42
|
+
# entity body, in the order they were applied. These will be set as
|
43
|
+
# the response's Content-Encoding header when it is sent to the client.
|
44
|
+
# Defaults to the empty Array.
|
45
|
+
attr_accessor :encodings
|
46
|
+
|
47
|
+
# The natural language(s) of the response's entity body. These will be
|
48
|
+
# set as the response's Content-Language header when it is sent to the
|
49
|
+
# client. Defaults to the empty Array.
|
50
|
+
attr_accessor :languages
|
51
|
+
|
52
|
+
|
53
|
+
### Overridden to add charset, encodings, and languages to outgoing
|
54
|
+
### headers if any of them are set.
|
55
|
+
def normalized_headers
|
56
|
+
headers = super
|
57
|
+
|
58
|
+
self.add_content_type_charset( headers )
|
59
|
+
headers.content_encoding ||= self.encodings.join(', ') unless self.encodings.empty?
|
60
|
+
headers.content_language ||= self.languages.join(', ') unless self.languages.empty?
|
61
|
+
|
62
|
+
return headers
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
### Overridden to reset charset, language, and encoding data, too.
|
67
|
+
def reset
|
68
|
+
super
|
69
|
+
|
70
|
+
@charset = nil
|
71
|
+
@languages.clear
|
72
|
+
@encodings.clear
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
#########
|
78
|
+
protected
|
79
|
+
#########
|
80
|
+
|
81
|
+
### Add a charset to the content-type header in +headers+ if possible.
|
82
|
+
def add_content_type_charset( headers )
|
83
|
+
charset = self.find_header_charset
|
84
|
+
self.log.debug "Setting the charset in the content-type header to: %p" % [ charset.name ]
|
85
|
+
|
86
|
+
headers.content_type.slice!( CONTENT_TYPE_CHARSET_RE ) and
|
87
|
+
self.log.debug " removed old charset parameter."
|
88
|
+
headers.content_type += "; charset=#{charset.name}" unless charset == Encoding::ASCII_8BIT
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
### Try to find a character set for the request, using the #charset attribute first,
|
93
|
+
### then the 'charset' parameter from the content-type header, then the Encoding object
|
94
|
+
### associated with the entity body, then the default external encoding (if it's set). If
|
95
|
+
### none of those are found, this method returns ISO-8859-1.
|
96
|
+
def find_header_charset
|
97
|
+
return ( self.charset ||
|
98
|
+
self.content_type_charset ||
|
99
|
+
self.entity_body_charset ||
|
100
|
+
Encoding.default_external ||
|
101
|
+
Encoding::ISO_8859_1 )
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
### Return an Encoding object for the 'charset' parameter of the content-type
|
106
|
+
### header, if there is one.
|
107
|
+
def content_type_charset
|
108
|
+
return nil unless self.content_type
|
109
|
+
name = self.content_type[ CONTENT_TYPE_CHARSET_RE, :charset ] or return nil
|
110
|
+
|
111
|
+
enc = Encoding.find( name )
|
112
|
+
self.log.debug "Extracted content-type charset: %p" % [ enc ]
|
113
|
+
|
114
|
+
return enc
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
### Get the body's charset, if possible. Returns +nil+ if the charset
|
119
|
+
### couldn't be determined.
|
120
|
+
def entity_body_charset
|
121
|
+
self.log.debug "Deriving charset from the entity body..."
|
122
|
+
|
123
|
+
# Have to use the instance variable instead of #body because plugins can
|
124
|
+
# override #body
|
125
|
+
|
126
|
+
if @body.respond_to?( :encoding )
|
127
|
+
self.log.debug " String-ish API. Encoding is: %p" % [ @body.encoding ]
|
128
|
+
return @body.encoding
|
129
|
+
elsif @body.respond_to?( :external_encoding )
|
130
|
+
self.log.debug " IO-ish API. Encoding is: %p" % [ @body.external_encoding ]
|
131
|
+
return @body.external_encoding
|
132
|
+
end
|
133
|
+
|
134
|
+
self.log.debug " Body didn't respond to either #encoding or #external_encoding."
|
135
|
+
return nil
|
136
|
+
end
|
137
|
+
|
138
|
+
end # class Strelka::HTTPResponse
|
139
|
+
|
140
|
+
|