acceptable 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ # encoding: binary
2
+
3
+ require 'rack/acceptable/utils'
4
+
5
+ module Rack #:nodoc:
6
+ module Acceptable #:nodoc:
7
+ module MIMETypes
8
+
9
+ module_function
10
+
11
+ MEDIA_RANGE_REGEX = /^\s*(#{Utils::TOKEN_PATTERN})\/(#{Utils::TOKEN_PATTERN})\s*$/o.freeze
12
+
13
+ #--
14
+ # RFC 2616, sec. 3.7:
15
+ # The type, subtype, and parameter attribute names are case-
16
+ # insensitive. Parameter values might or might not be case-sensitive,
17
+ # depending on the semantics of the parameter name. Linear white space
18
+ # (LWS) MUST NOT be used between the type and subtype, nor between an
19
+ # attribute and its value. The presence or absence of a parameter might
20
+ # be significant to the processing of a media-type, depending on its
21
+ # definition within the media type registry.
22
+ #++
23
+
24
+ # ==== Parameters
25
+ # thing<String>::
26
+ # The Media-Type snippet or the single item from the HTTP_ACCEPT
27
+ # request-header, *without* 'q' parameter, accept-extensions and so on.
28
+ #
29
+ # ==== Returns
30
+ # Array[String, String, Hash]::
31
+ # Media-Range: type, subtype and parameter (as a +Hash+).
32
+ #
33
+ # ==== Raises
34
+ # Same things as Utils#split_mime_type.
35
+ # In other words, it checks only type/subtype pair.
36
+ #
37
+ def parse_media_range(thing)
38
+ snippets = thing.split(Utils::SEMICOLON_SPLITTER)
39
+ raise ArgumentError, "Malformed MIME-Type: #{thing}" unless MEDIA_RANGE_REGEX === snippets.shift
40
+
41
+ type = $1
42
+ subtype = $2
43
+ type.downcase!
44
+ subtype.downcase!
45
+
46
+ raise ArgumentError,
47
+ "Malformed MIME-Type: #{thing}" if type == Const::WILDCARD && subtype != Const::WILDCARD
48
+
49
+ params = {}
50
+ snippets.each do |pair|
51
+ pair.strip!
52
+ k,v = pair.split(Utils::PAIR_SPLITTER,2)
53
+ k.downcase!
54
+ params[k] = v
55
+ end
56
+
57
+ [type, subtype, params]
58
+ end
59
+
60
+ # ==== Parameters
61
+ # thing<String>::
62
+ # The Media-Type snippet or the single item from the HTTP_ACCEPT request-header.
63
+ #
64
+ # ==== Returns
65
+ # Array[String, String, Hash, Float, Hash]::
66
+ # Media-Range (type, subtype and parameter, as a +Hash+), quality factor
67
+ # and accept-extension (as a +Hash+, if any, or +nil+) of the MIME-Type.
68
+ #
69
+ # ==== Raises
70
+ # ArgumentError::
71
+ # There's a malformed quality factor, or type/subtype pair
72
+ # is not in a RFC 'Media-Range' pattern.
73
+ #
74
+ def parse_mime_type(thing)
75
+
76
+ snippets = thing.split(Utils::SEMICOLON_SPLITTER)
77
+ raise ArgumentError, "Malformed MIME-Type: #{thing}" unless MEDIA_RANGE_REGEX === snippets.shift
78
+
79
+ type = $1
80
+ subtype = $2
81
+ type.downcase!
82
+ subtype.downcase!
83
+
84
+ raise ArgumentError,
85
+ "Malformed MIME-Type: #{thing}" if type == Const::WILDCARD && subtype != Const::WILDCARD
86
+
87
+ qvalue = Utils::QVALUE_DEFAULT
88
+ params = {}
89
+ has_qvalue = false
90
+ accept_extension = nil
91
+
92
+ for pair in snippets
93
+ pair.strip!
94
+ k,v = pair.split(Utils::PAIR_SPLITTER,2)
95
+
96
+ # RFC 2616, sec. 14.1:
97
+ # Each media-range MAY be followed by one or more accept-params,
98
+ # beginning with the "q" parameter for indicating a relative quality
99
+ # factor. The first "q" parameter (if any) separates the media-range
100
+ # parameter(s) from the accept-params. Quality factors allow the user
101
+ # or user agent to indicate the relative degree of preference for that
102
+ # media-range, using the qvalue scale from 0 to 1 (section 3.9). The
103
+ # default value is q=1.
104
+
105
+ if has_qvalue
106
+ accept_extension ||= {}
107
+ accept_extension[k] = v || true # token [ "=" ( token | quoted-string ) ] - i.e, "v" is OPTIONAL.
108
+ else
109
+ k.downcase!
110
+ if k == Utils::QVALUE
111
+ raise ArgumentError, "Malformed quality factor: #{v.inspect}" unless Utils::QVALUE_REGEX === v
112
+ qvalue = v.to_f
113
+ has_qvalue = true
114
+ else
115
+ params[k] = v
116
+ end
117
+ end
118
+
119
+ end
120
+
121
+ [type, subtype, params, qvalue, accept_extension]
122
+ end
123
+
124
+ # ==== Parameters
125
+ # thing<String, Array>:: The Media-Type snippet or *parsed* Media-Type.
126
+ # types<Array>:: Parsed HTTP_ACCEPT request-header to check against.
127
+ #
128
+ # ==== Returns
129
+ # Float:: The quality factor (relative strength of the Media-Type).
130
+ #
131
+ def qualify_mime_type(thing, types)
132
+ weigh_mime_type(thing, types).first
133
+ end
134
+
135
+ # ==== Parameters
136
+ # thing<String, Array>:: The Media-Type snippet or *parsed* Media-Type.
137
+ # types<Array>:: Parsed HTTP_ACCEPT request-header to check against.
138
+ # qvalue_only<Boolean>:: Flag to force weighting to return the qvalue only.
139
+ # Optional. Default is +false+.
140
+ #
141
+ # ==== Returns
142
+ # Array[Float, Integer, Integer, Integer] or Array[Float]::
143
+ # Quality factor, rate, specificity and negated index of the most relevant Media-Range;
144
+ # i.e full relative weight of the Media-Type. If +qvaulue_only+ option is set to true,
145
+ # returns qvalue only.
146
+ #
147
+ def weigh_mime_type(thing, types, qvalue_only = false)
148
+
149
+ type, subtype, params = thing.is_a?(String) ? parse_media_range(thing) : thing
150
+
151
+ rate = 0
152
+ specificity = -1
153
+ quality = 0.00
154
+ index = 0
155
+
156
+ # RFC 2616, sec. 14.1:
157
+ # Media ranges can be overridden by more specific media ranges or
158
+ # specific media types. If more than one media range applies to a given
159
+ # type, the most specific reference has precedence.
160
+ # ...
161
+ # The media type quality factor associated with a given type is
162
+ # determined by finding the media range with the highest precedence
163
+ # which matches that type.
164
+
165
+ type_is_a_wildcard = type == Const::WILDCARD
166
+ subtype_is_a_wildcard = subtype == Const::WILDCARD
167
+
168
+ types.each_with_index do |(t,s,p,q),i|
169
+ next unless (type_is_a_wildcard || t == type || no_type_match = t == Const::WILDCARD) &&
170
+ (subtype_is_a_wildcard || s == subtype || no_subtype_match = s == Const::WILDCARD)
171
+
172
+ # we should skip when:
173
+ # - divergence:
174
+ # * "text;html;a=2" against "text/html;a=1,text/*;a=1" etc
175
+ # * "text/html;b=1" or "text/html" against "text/html;a=1" etc,
176
+ # i.e, 'a' parameter is NECESSARY, but our MIME-Type does NOT contain it
177
+ # - rate is lesser
178
+ # - rates are equal, but sp(ecificity) is lesser or exactly the same
179
+
180
+ r = no_type_match ? 0 : 10
181
+ r += no_subtype_match ? 0 : 1
182
+
183
+ next if r < rate
184
+
185
+ sp = 0
186
+ p.each do |k,v|
187
+ if params.key?(k) && params[k] == v
188
+ sp += 1
189
+ else
190
+ sp = -1
191
+ break
192
+ end
193
+ end
194
+
195
+ #next if sp == -1 || (r == rate && (sp < specificity || sp == specificity && quality > q))
196
+ if sp > -1 && (r > rate || (sp > specificity || sp == specificity && quality < q))
197
+ specificity = sp
198
+ rate = r
199
+ quality = q
200
+ index = i
201
+ end
202
+ end
203
+
204
+ qvalue_only ? [quality] : [quality, rate, specificity, -index]
205
+ end
206
+
207
+ # ==== Parameters
208
+ # provides<Array>:: The Array of available Media-Types (snippets or parsed). Could be empty.
209
+ # accepts<String>:: The Array of acceptable Media-Ranges. Could be empty.
210
+ # by_qvalue_only<String>:: Optional flag, see MIMETypes#weigh_mime_type. Default is +false+.
211
+ #
212
+ # ==== Returns
213
+ # The best one of available Media-Types or +nil+.
214
+ #
215
+ # ==== Raises
216
+ # Same things as Utils#parse_media_range.
217
+ #
218
+ # ==== Notes
219
+ # Acceptable Media-Types are supposed to have *downcased* and *well-formed*
220
+ # type, subtype, parameter's keys (according to RFC 2616, enumerated things
221
+ # are case-insensitive too), and *sensible* qvalues ("real numbers in the
222
+ # range 0 through 1, where 0 is the minimum and 1 the maximum value").
223
+ #
224
+ def detect_best_mime_type(provides, accepts, by_qvalue_only = false)
225
+ return nil if provides.empty?
226
+ return provides.first if accepts.empty?
227
+ i = 1
228
+ candidate = provides.map { |t| weigh_mime_type(t,accepts,by_qvalue_only) << i-=1 }.max
229
+ candidate.at(0) == 0 ? nil : provides.at(-candidate.last)
230
+ end
231
+
232
+ REGISTRY_PATH = ::File.expand_path(::File.join(::File.dirname(__FILE__), 'data', 'mime.types')).freeze
233
+ REGISTRY = {}
234
+ EXTENSIONS = {}
235
+
236
+ # Registers the new MIME-Type and associated extensions.
237
+ # The first one of extensions will be treated as the 'preferred'
238
+ # for the MIME-Type.
239
+ #
240
+ def register(thing, *extensions)
241
+ return if extensions.empty?
242
+ extensions.map! { |ext| ext[0] == ?. ? ext.downcase : ".#{ext.downcase}" }
243
+ extensions.each { |ext| REGISTRY[ext] = thing }
244
+ EXTENSIONS[thing] = extensions.first
245
+ nil
246
+ end
247
+
248
+ # Deletes the MIME-Type (and associated extensions) from registry.
249
+ def delete(thing)
250
+ REGISTRY.delete_if { |_,v| v == thing }
251
+ EXTENSIONS.delete thing
252
+ end
253
+
254
+ def lookup(ext, fallback = 'application/octet-stream')
255
+ REGISTRY.fetch(ext[0] == ?. ? ext.downcase : ".#{ext.downcase}", fallback)
256
+ end
257
+
258
+ def extension_for(thing, fallback = nil)
259
+ EXTENSIONS.fetch thing, fallback
260
+ end
261
+
262
+ # Empties the registry.
263
+ def clear
264
+ EXTENSIONS.clear
265
+ REGISTRY.clear
266
+ nil
267
+ end
268
+
269
+ # Resets the registry, i.e removes all and loads
270
+ # the default set of the MIME-Types.
271
+ def reset
272
+ clear
273
+ load_from(REGISTRY_PATH)
274
+ end
275
+
276
+ # Loads the set of MIME-Types from the Apache compatible mime.types file.
277
+ # original source: webrick.
278
+ def load_from(file)
279
+ open(file) do |io|
280
+ io.each do |line|
281
+ line.strip!
282
+ next if line.empty? || /^#/ === line
283
+ register *line.split(/\s+/)
284
+ end
285
+ end
286
+ true
287
+ end
288
+
289
+ end
290
+ end
291
+ end
292
+
293
+ # EOF
@@ -0,0 +1,88 @@
1
+ require 'rack/acceptable/utils'
2
+
3
+ module Rack #:nodoc:
4
+ module Acceptable #:nodoc:
5
+ module Headers
6
+
7
+ # ==== Returns
8
+ # An Array with wildcards / *downcased* Content-Codings
9
+ # and associated quality factors (qvalues). Default qvalue is 1.0.
10
+ #
11
+ # ==== Raises
12
+ # ArgumentError::
13
+ # Syntax of the Accept-Encoding request-header is bad.
14
+ # For example, one of Content-Codings is not a 'token',
15
+ # one of quality factors is malformed etc.
16
+ #
17
+ def acceptable_encodings
18
+ Utils.parse_header(
19
+ env[Const::ENV_HTTP_ACCEPT_ENCODING].to_s.downcase,
20
+ Utils::HTTP_ACCEPT_TOKEN_REGEX)
21
+ rescue
22
+ raise ArgumentError,
23
+ "Malformed Accept-Encoding header: #{env[Const::ENV_HTTP_ACCEPT_ENCODING].inspect}"
24
+ end
25
+
26
+ # ==== Returns
27
+ # An Array with wildcards / *downcased* Charsets and
28
+ # associated quality factors (qvalues). Default qvalue is 1.0.
29
+ #
30
+ # ==== Raises
31
+ # ArgumentError::
32
+ # Syntax of the Accept-Charset request-header is bad.
33
+ # For example, one of Charsets is not a 'token',
34
+ # one of quality factors is malformed etc.
35
+ #
36
+ def acceptable_charsets
37
+ Utils.parse_header(
38
+ env[Const::ENV_HTTP_ACCEPT_CHARSET].to_s.downcase,
39
+ Utils::HTTP_ACCEPT_TOKEN_REGEX)
40
+ rescue
41
+ raise ArgumentError,
42
+ "Malformed Accept-Charset header: #{env[Const::ENV_HTTP_ACCEPT_CHARSET].inspect}"
43
+ end
44
+
45
+ # ==== Returns
46
+ # An Array with wildcards / Language-Tags (as +Strings+)
47
+ # and associated quality factors (qvalues). Default qvalue is 1.0.
48
+ #
49
+ # ==== Raises
50
+ # ArgumentError::
51
+ # Syntax of the Accept-Language request-header is bad.
52
+ # For example, one of Language-Ranges is not in a RFC 'Language-Range'
53
+ # pattern, one of quality factors is malformed etc.
54
+ #
55
+ # ==== Notes
56
+ # * It uses {Extended Language-Range pattern}[http://tools.ietf.org/html/rfc4647#section-2.2].
57
+ # * It does *not* perform 'convenient transformations' (downcasing of primary tags etc).
58
+ # In other words, it parses Accept-Language header in unpretentious manner.
59
+ #
60
+ def acceptable_language_ranges
61
+ Utils.parse_header(
62
+ env[Const::ENV_HTTP_ACCEPT_LANGUAGE].to_s,
63
+ Utils::HTTP_ACCEPT_LANGUAGE_REGEX)
64
+ rescue
65
+ raise ArgumentError,
66
+ "Malformed Accept-Language header: #{env[Const::ENV_HTTP_ACCEPT_LANGUAGE].inspect}"
67
+ end
68
+
69
+ # ==== Returns
70
+ # An Array with Media-Ranges (as +Strings+) / wildcards and
71
+ # associated qvalues. Default qvalue is 1.0.
72
+ #
73
+ # ==== Raises
74
+ # ArgumentError::
75
+ # There's a malformed qvalue in header.
76
+ #
77
+ def acceptable_media_ranges
78
+ Utils.extract_qvalues(env[Const::ENV_HTTP_ACCEPT].to_s)
79
+ rescue
80
+ raise ArgumentError,
81
+ "Malformed Accept header: #{env[Const::ENV_HTTP_ACCEPT].inspect}"
82
+ end
83
+
84
+ end
85
+ end
86
+ end
87
+
88
+ # EOF
@@ -0,0 +1,56 @@
1
+ require 'rack/acceptable/mimetypes'
2
+
3
+ module Rack #:nodoc:
4
+ module Acceptable #:nodoc:
5
+ module Media
6
+
7
+ # ==== Returns
8
+ # An Array with *completely* parsed MIME-Types (incl. qvalues
9
+ # and accept-extensions; see Rack::Acceptable::MIMETypes).
10
+ # Default qvalue is 1.0.
11
+ #
12
+ # ==== Raises
13
+ # ArgumentError::
14
+ # Syntax of the The Accept request-header is bad.
15
+ # For example, one of Media-Ranges is not in a RFC 'Media-Range'
16
+ # pattern (type or subtype is invalid, or there's something like "*/foo")
17
+ # or, at last, one of MIME-Types has malformed qvalue.
18
+ #
19
+ def acceptable_media
20
+ @_acceptable_media ||= begin
21
+ header = env[Const::ENV_HTTP_ACCEPT].to_s
22
+ header.split(Utils::COMMA_SPLITTER).map! { |entry| MIMETypes.parse_mime_type(entry) }
23
+ end
24
+ end
25
+
26
+ # Checks if the MIME-Type passed acceptable.
27
+ def accept_media?(thing)
28
+ qvalue = MIMETypes.weigh_mime_type(thing, acceptable_media).first
29
+ qvalue > 0
30
+ rescue
31
+ false
32
+ end
33
+
34
+ # Returns the best match for the MIME-Type or
35
+ # pattern (like "text/*" etc) passed or +nil+.
36
+ def best_media_for(thing)
37
+ weight = MIMETypes.weigh_mime_type(thing, acceptable_media)
38
+ if weight.first > 0
39
+ acceptable_media.at(-weight.last)
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ def negotiate_media(*things)
46
+ flag = (things.last == true || things.last == false) ? things.pop : false
47
+ MIMETypes.detect_best_mime_type(things, acceptable_media, flag)
48
+ end
49
+
50
+ alias :preferred_media_from :negotiate_media
51
+
52
+ end
53
+ end
54
+ end
55
+
56
+ # EOF
@@ -0,0 +1,45 @@
1
+ require 'rack/request'
2
+ require 'rack/acceptable/mixin/headers'
3
+ require 'rack/acceptable/mixin/media'
4
+
5
+ module Rack #:nodoc:
6
+ module Acceptable #:nodoc:
7
+ class Request < Rack::Request
8
+ include Rack::Acceptable::Headers
9
+ include Rack::Acceptable::Media
10
+
11
+ def acceptable_charsets
12
+ @_acceptable_charsets ||= super
13
+ end
14
+
15
+ def accept_charset?(chs)
16
+ chs = chs.downcase
17
+ accepts = acceptable_charsets
18
+ return true if accepts.empty?
19
+ if ch = accepts.assoc(chs) || accepts.assoc(Const::WILDCARD)
20
+ ch.last > 0
21
+ else
22
+ chs == Const::ISO_8859_1
23
+ end
24
+ rescue
25
+ false
26
+ end
27
+
28
+ def accept_content?(content_type)
29
+ media = MIMETypes.parse_media_range(content_type)
30
+ chs = media.last.delete(Const::CHARSET)
31
+ chs ||= Const::ISO_8859_1 if media.first == Const::TEXT
32
+ if chs
33
+ accept_media?(media) && accept_charset?(chs)
34
+ else
35
+ accept_media?(media)
36
+ end
37
+ rescue
38
+ false
39
+ end
40
+
41
+ end
42
+ end
43
+ end
44
+
45
+ # EOF