strelka 0.0.1pre4 → 0.0.1.pre129
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|