hanami-controller 2.0.0.alpha8 → 2.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,7 @@
1
- require 'hanami/utils/hash'
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/utils"
4
+ require "hanami/utils/hash"
2
5
 
3
6
  module Hanami
4
7
  class Action
@@ -10,40 +13,9 @@ module Hanami
10
13
  #
11
14
  # @see Hanami::Action::Cookies#cookies
12
15
  class CookieJar
13
- # The key that returns raw cookies from the Rack env
14
- #
15
- # @since 0.1.0
16
- # @api private
17
- HTTP_HEADER = 'HTTP_COOKIE'.freeze
18
-
19
- # The key used by Rack to set the session cookie
20
- #
21
- # We let CookieJar to NOT take care of this cookie, but it leaves the
22
- # responsibility to the Rack middleware that handle sessions.
23
- #
24
- # This prevents <tt>Set-Cookie</tt> to be sent twice.
25
- #
26
- # @since 0.5.1
27
- # @api private
28
- #
29
- # @see https://github.com/hanami/controller/issues/138
30
- RACK_SESSION_KEY = :'rack.session'
31
-
32
- # The key used by Rack to set the cookies as an Hash in the env
33
- #
34
- # @since 0.1.0
35
- # @api private
36
- COOKIE_HASH_KEY = 'rack.request.cookie_hash'.freeze
37
-
38
- # The key used by Rack to set the cookies as a String in the env
39
- #
40
- # @since 0.1.0
41
- # @api private
42
- COOKIE_STRING_KEY = 'rack.request.cookie_string'.freeze
43
-
44
16
  # @since 0.4.5
45
17
  # @api private
46
- COOKIE_SEPARATOR = ';,'.freeze
18
+ COOKIE_SEPARATOR = ";,"
47
19
 
48
20
  # Initialize the CookieJar
49
21
  #
@@ -68,11 +40,14 @@ module Hanami
68
40
  #
69
41
  # @see Hanami::Action::Cookies#finish
70
42
  def finish
71
- @cookies.delete(RACK_SESSION_KEY)
72
- @cookies.each do |k,v|
73
- next unless changed?(k)
74
- v.nil? ? delete_cookie(k) : set_cookie(k, _merge_default_values(v))
75
- end if changed?
43
+ @cookies.delete(Action::RACK_SESSION)
44
+ if changed?
45
+ @cookies.each do |k, v|
46
+ next unless changed?(k)
47
+
48
+ v.nil? ? delete_cookie(k) : set_cookie(k, _merge_default_values(v))
49
+ end
50
+ end
76
51
  end
77
52
 
78
53
  # Returns the object associated with the given key
@@ -122,12 +97,12 @@ module Hanami
122
97
  #
123
98
  # @example
124
99
  # require "hanami/controller"
125
- # class MyAction
126
- # include Hanami::Action
100
+ # class MyAction < Hanami::Action
127
101
  # include Hanami::Action::Cookies
128
102
  #
129
- # def call(params)
130
- # cookies.each do |key, value|
103
+ # def handle(req, res)
104
+ # # read cookies
105
+ # req.cookies.each do |key, value|
131
106
  # # ...
132
107
  # end
133
108
  # end
@@ -167,10 +142,10 @@ module Hanami
167
142
  # @api private
168
143
  def _merge_default_values(value)
169
144
  cookies_options = if value.is_a?(::Hash)
170
- value.merge! _add_expires_option(value)
171
- else
172
- { value: value }
173
- end
145
+ value.merge! _add_expires_option(value)
146
+ else
147
+ {value: value}
148
+ end
174
149
  @default_options.merge cookies_options
175
150
  end
176
151
 
@@ -179,8 +154,8 @@ module Hanami
179
154
  # @since 0.4.3
180
155
  # @api private
181
156
  def _add_expires_option(value)
182
- if value.has_key?(:max_age) && !value.has_key?(:expires)
183
- { expires: (Time.now + value[:max_age]) }
157
+ if value.key?(:max_age) && !value.key?(:expires)
158
+ {expires: (Time.now + value[:max_age])}
184
159
  else
185
160
  {}
186
161
  end
@@ -193,11 +168,12 @@ module Hanami
193
168
  # @since 0.1.0
194
169
  # @api private
195
170
  def extract(env)
196
- hash = env[COOKIE_HASH_KEY] ||= {}
197
- string = env[HTTP_HEADER]
171
+ hash = env[Action::COOKIE_HASH_KEY] ||= {}
172
+ string = env[Action::HTTP_COOKIE]
173
+
174
+ return hash if string == env[Action::COOKIE_STRING_KEY]
198
175
 
199
- return hash if string == env[COOKIE_STRING_KEY]
200
- # TODO Next Rack 1.7.x ?? version will have ::Rack::Utils.parse_cookies
176
+ # TODO: Next Rack 1.7.x ?? version will have ::Rack::Utils.parse_cookies
201
177
  # We can then replace the following lines.
202
178
  hash.clear
203
179
 
@@ -206,9 +182,15 @@ module Hanami
206
182
  # the Cookie header such that those with more specific Path attributes
207
183
  # precede those with less specific. Ordering with respect to other
208
184
  # attributes (e.g., Domain) is unspecified.
209
- cookies = ::Rack::Utils.parse_query(string, COOKIE_SEPARATOR) { |s| ::Rack::Utils.unescape(s) rescue s }
210
- cookies.each { |k,v| hash[k] = Array === v ? v.first : v }
211
- env[COOKIE_STRING_KEY] = string
185
+ cookies = ::Rack::Utils.parse_query(string, COOKIE_SEPARATOR) { |s|
186
+ begin
187
+ ::Rack::Utils.unescape(s)
188
+ rescue StandardError
189
+ s
190
+ end
191
+ }
192
+ cookies.each { |k, v| hash[k] = v.is_a?(Array) ? v.first : v }
193
+ env[Action::COOKIE_STRING_KEY] = string
212
194
  hash
213
195
  end
214
196
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Hanami
2
4
  class Action
3
5
  # Cookies API
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "hanami/utils/blank"
4
+ require "hanami/controller/error"
2
5
  require "rack/utils"
3
6
  require "securerandom"
4
7
 
@@ -8,7 +11,7 @@ module Hanami
8
11
  # Invalid CSRF Token
9
12
  #
10
13
  # @since 0.4.0
11
- class InvalidCSRFTokenError < ::StandardError
14
+ class InvalidCSRFTokenError < Controller::Error
12
15
  end
13
16
 
14
17
  # CSRF Protection
@@ -38,10 +41,8 @@ module Hanami
38
41
  #
39
42
  # @example Custom Handling
40
43
  # module Web::Controllers::Books
41
- # class Create
42
- # include Web::Action
43
- #
44
- # def call(params)
44
+ # class Create < Web::Action
45
+ # def handl(*)
45
46
  # # ...
46
47
  # end
47
48
  #
@@ -56,16 +57,14 @@ module Hanami
56
57
  #
57
58
  # @example Bypass Security Check
58
59
  # module Web::Controllers::Books
59
- # class Create
60
- # include Web::Action
61
- #
62
- # def call(params)
60
+ # class Create < Web::Action
61
+ # def handle(*)
63
62
  # # ...
64
63
  # end
65
64
  #
66
65
  # private
67
66
  #
68
- # def verify_csrf_token?
67
+ # def verify_csrf_token?(req, res)
69
68
  # false
70
69
  # end
71
70
  # end
@@ -87,18 +86,20 @@ module Hanami
87
86
  # @since 0.4.0
88
87
  # @api private
89
88
  IDEMPOTENT_HTTP_METHODS = Hash[
90
- "GET" => true,
91
- "HEAD" => true,
92
- "TRACE" => true,
93
- "OPTIONS" => true
89
+ Action::GET => true,
90
+ Action::HEAD => true,
91
+ Action::TRACE => true,
92
+ Action::OPTIONS => true
94
93
  ].freeze
95
94
 
96
95
  # @since 0.4.0
97
96
  # @api private
98
97
  def self.included(action)
99
- action.class_eval do
100
- before :set_csrf_token, :verify_csrf_token
101
- end unless Hanami.respond_to?(:env?) && Hanami.env?(:test)
98
+ unless Hanami.respond_to?(:env?) && Hanami.env?(:test)
99
+ action.class_eval do
100
+ before :set_csrf_token, :verify_csrf_token
101
+ end
102
+ end
102
103
  end
103
104
 
104
105
  private
@@ -107,7 +108,7 @@ module Hanami
107
108
  #
108
109
  # @since 0.4.0
109
110
  # @api private
110
- def set_csrf_token(req, res)
111
+ def set_csrf_token(_req, res)
111
112
  res.session[CSRF_TOKEN] ||= generate_csrf_token
112
113
  end
113
114
 
@@ -141,7 +142,7 @@ module Hanami
141
142
  # Verify the CSRF token was passed in params.
142
143
  #
143
144
  # @api private
144
- def missing_csrf_token?(req, res)
145
+ def missing_csrf_token?(req, *)
145
146
  Hanami::Utils::Blank.blank?(req.params[CSRF_TOKEN])
146
147
  end
147
148
 
@@ -161,21 +162,19 @@ module Hanami
161
162
  #
162
163
  # @example
163
164
  # module Web::Controllers::Books
164
- # class Create
165
- # include Web::Action
166
- #
167
- # def call(params)
165
+ # class Create < Web::Action
166
+ # def call(*)
168
167
  # # ...
169
168
  # end
170
169
  #
171
170
  # private
172
171
  #
173
- # def verify_csrf_token?
172
+ # def verify_csrf_token?(req, res)
174
173
  # false
175
174
  # end
176
175
  # end
177
176
  # end
178
- def verify_csrf_token?(req, res)
177
+ def verify_csrf_token?(req, *)
179
178
  !IDEMPOTENT_HTTP_METHODS[req.request_method]
180
179
  end
181
180
 
@@ -191,21 +190,19 @@ module Hanami
191
190
  #
192
191
  # @example
193
192
  # module Web::Controllers::Books
194
- # class Create
195
- # include Web::Action
196
- #
197
- # def call(params)
193
+ # class Create < Web::Action
194
+ # def call(*)
198
195
  # # ...
199
196
  # end
200
197
  #
201
198
  # private
202
199
  #
203
- # def handle_invalid_csrf_token
200
+ # def handle_invalid_csrf_token(req, res)
204
201
  # # custom invalid CSRF management goes here
205
202
  # end
206
203
  # end
207
204
  # end
208
- def handle_invalid_csrf_token(req, res)
205
+ def handle_invalid_csrf_token(*, res)
209
206
  res.session.clear
210
207
  raise InvalidCSRFTokenError
211
208
  end
@@ -18,6 +18,10 @@ module Hanami
18
18
  # @since 0.3.0
19
19
  # @api public
20
20
  class Flash
21
+ # @since 2.0.0
22
+ # @api private
23
+ KEY = "_flash"
24
+
21
25
  # @return [Hash] The flash hash for the next request, written to by {#[]=}.
22
26
  #
23
27
  # @see #[]=
@@ -1,8 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "hanami/http/status"
2
4
 
3
5
  module Hanami
4
6
  class Action
5
7
  module Halt
8
+ # @since 2.0.0
9
+ # @api private
6
10
  def self.call(status, body = nil)
7
11
  body ||= Http::Status.message_for(status)
8
12
  throw :halt, [status, body]
@@ -1,85 +1,74 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "hanami/utils"
2
4
  require "rack/utils"
3
5
  require "rack/mime"
4
6
 
5
7
  module Hanami
6
8
  class Action
7
- module Mime
8
- DEFAULT_CONTENT_TYPE = 'application/octet-stream'.freeze
9
- DEFAULT_CHARSET = 'utf-8'.freeze
10
-
11
- # The key that returns content mime type from the Rack env
12
- #
13
- # @since 2.0.0
14
- # @api private
15
- HTTP_CONTENT_TYPE = 'CONTENT_TYPE'.freeze
16
-
17
- # The header key to set the mime type of the response
18
- #
19
- # @since 0.1.0
20
- # @api private
21
- CONTENT_TYPE = 'Content-Type'.freeze
22
-
9
+ module Mime # rubocop:disable Metrics/ModuleLength
23
10
  # Most commom MIME Types used for responses
24
11
  #
25
12
  # @since 1.0.0
26
13
  # @api private
27
14
  TYPES = {
28
- txt: 'text/plain',
29
- html: 'text/html',
30
- json: 'application/json',
31
- manifest: 'text/cache-manifest',
32
- atom: 'application/atom+xml',
33
- avi: 'video/x-msvideo',
34
- bmp: 'image/bmp',
35
- bz: 'application/x-bzip',
36
- bz2: 'application/x-bzip2',
37
- chm: 'application/vnd.ms-htmlhelp',
38
- css: 'text/css',
39
- csv: 'text/csv',
40
- flv: 'video/x-flv',
41
- gif: 'image/gif',
42
- gz: 'application/x-gzip',
43
- h264: 'video/h264',
44
- ico: 'image/vnd.microsoft.icon',
45
- ics: 'text/calendar',
46
- jpg: 'image/jpeg',
47
- js: 'application/javascript',
48
- mp4: 'video/mp4',
49
- mov: 'video/quicktime',
50
- mp3: 'audio/mpeg',
51
- mp4a: 'audio/mp4',
52
- mpg: 'video/mpeg',
53
- oga: 'audio/ogg',
54
- ogg: 'application/ogg',
55
- ogv: 'video/ogg',
56
- pdf: 'application/pdf',
57
- pgp: 'application/pgp-encrypted',
58
- png: 'image/png',
59
- psd: 'image/vnd.adobe.photoshop',
60
- rss: 'application/rss+xml',
61
- rtf: 'application/rtf',
62
- sh: 'application/x-sh',
63
- svg: 'image/svg+xml',
64
- swf: 'application/x-shockwave-flash',
65
- tar: 'application/x-tar',
66
- torrent: 'application/x-bittorrent',
67
- tsv: 'text/tab-separated-values',
68
- uri: 'text/uri-list',
69
- vcs: 'text/x-vcalendar',
70
- wav: 'audio/x-wav',
71
- webm: 'video/webm',
72
- wmv: 'video/x-ms-wmv',
73
- woff: 'application/font-woff',
74
- woff2: 'application/font-woff2',
75
- wsdl: 'application/wsdl+xml',
76
- xhtml: 'application/xhtml+xml',
77
- xml: 'application/xml',
78
- xslt: 'application/xslt+xml',
79
- yml: 'text/yaml',
80
- zip: 'application/zip'
15
+ txt: "text/plain",
16
+ html: "text/html",
17
+ json: "application/json",
18
+ manifest: "text/cache-manifest",
19
+ atom: "application/atom+xml",
20
+ avi: "video/x-msvideo",
21
+ bmp: "image/bmp",
22
+ bz: "application/x-bzip",
23
+ bz2: "application/x-bzip2",
24
+ chm: "application/vnd.ms-htmlhelp",
25
+ css: "text/css",
26
+ csv: "text/csv",
27
+ flv: "video/x-flv",
28
+ gif: "image/gif",
29
+ gz: "application/x-gzip",
30
+ h264: "video/h264",
31
+ ico: "image/vnd.microsoft.icon",
32
+ ics: "text/calendar",
33
+ jpg: "image/jpeg",
34
+ js: "application/javascript",
35
+ mp4: "video/mp4",
36
+ mov: "video/quicktime",
37
+ mp3: "audio/mpeg",
38
+ mp4a: "audio/mp4",
39
+ mpg: "video/mpeg",
40
+ oga: "audio/ogg",
41
+ ogg: "application/ogg",
42
+ ogv: "video/ogg",
43
+ pdf: "application/pdf",
44
+ pgp: "application/pgp-encrypted",
45
+ png: "image/png",
46
+ psd: "image/vnd.adobe.photoshop",
47
+ rss: "application/rss+xml",
48
+ rtf: "application/rtf",
49
+ sh: "application/x-sh",
50
+ svg: "image/svg+xml",
51
+ swf: "application/x-shockwave-flash",
52
+ tar: "application/x-tar",
53
+ torrent: "application/x-bittorrent",
54
+ tsv: "text/tab-separated-values",
55
+ uri: "text/uri-list",
56
+ vcs: "text/x-vcalendar",
57
+ wav: "audio/x-wav",
58
+ webm: "video/webm",
59
+ wmv: "video/x-ms-wmv",
60
+ woff: "application/font-woff",
61
+ woff2: "application/font-woff2",
62
+ wsdl: "application/wsdl+xml",
63
+ xhtml: "application/xhtml+xml",
64
+ xml: "application/xml",
65
+ xslt: "application/xslt+xml",
66
+ yml: "text/yaml",
67
+ zip: "application/zip"
81
68
  }.freeze
82
69
 
70
+ # @since 2.0.0
71
+ # @api private
83
72
  def self.content_type_with_charset(content_type, charset)
84
73
  "#{content_type}; charset=#{charset}"
85
74
  end
@@ -92,27 +81,38 @@ module Hanami
92
81
  # lastly it will fallback to DEFAULT_CONTENT_TYPE
93
82
  #
94
83
  # @return [String]
84
+ #
85
+ # @since 2.0.0
86
+ # @api private
95
87
  def self.content_type(configuration, request, accepted_mime_types)
96
88
  if request.accept_header?
97
89
  type = best_q_match(request.accept, accepted_mime_types)
98
90
  return type if type
99
91
  end
100
92
 
101
- default_response_type(configuration) || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
93
+ default_response_type(configuration) || default_content_type(configuration) || Action::DEFAULT_CONTENT_TYPE
102
94
  end
103
95
 
96
+ # @since 2.0.0
97
+ # @api private
104
98
  def self.charset(default_charset)
105
- default_charset || DEFAULT_CHARSET
99
+ default_charset || Action::DEFAULT_CHARSET
106
100
  end
107
101
 
102
+ # @since 2.0.0
103
+ # @api private
108
104
  def self.default_response_type(configuration)
109
105
  format_to_mime_type(configuration.default_response_format, configuration)
110
106
  end
111
107
 
108
+ # @since 2.0.0
109
+ # @api private
112
110
  def self.default_content_type(configuration)
113
111
  format_to_mime_type(configuration.default_request_format, configuration)
114
112
  end
115
113
 
114
+ # @since 2.0.0
115
+ # @api private
116
116
  def self.format_to_mime_type(format, configuration)
117
117
  return if format.nil?
118
118
 
@@ -128,12 +128,18 @@ module Hanami
128
128
  # detect_format("text/html; charset=utf-8", configuration) #=> :html
129
129
  #
130
130
  # @return [Symbol, nil]
131
+ #
132
+ # @since 2.0.0
133
+ # @api private
131
134
  def self.detect_format(content_type, configuration)
132
135
  return if content_type.nil?
136
+
133
137
  ct = content_type.split(";").first
134
138
  configuration.format_for(ct) || format_for(ct)
135
139
  end
136
140
 
141
+ # @since 2.0.0
142
+ # @api private
137
143
  def self.format_for(content_type)
138
144
  TYPES.key(content_type)
139
145
  end
@@ -145,6 +151,9 @@ module Hanami
145
151
  # @return [Array<String>, nil]
146
152
  #
147
153
  # @raise [Hanami::Controller::UnknownFormatError] if the format is invalid
154
+ #
155
+ # @since 2.0.0
156
+ # @api private
148
157
  def self.restrict_mime_types(configuration, accepted_formats)
149
158
  return if accepted_formats.empty?
150
159
 
@@ -155,6 +164,7 @@ module Hanami
155
164
  accepted_mime_types = mime_types & configuration.mime_types
156
165
 
157
166
  return if accepted_mime_types.empty?
167
+
158
168
  accepted_mime_types
159
169
  end
160
170
 
@@ -164,8 +174,13 @@ module Hanami
164
174
  # If no Content-Type is sent in the request it will check the default_request_format
165
175
  #
166
176
  # @return [TrueClass, FalseClass]
177
+ #
178
+ # @since 2.0.0
179
+ # @api private
167
180
  def self.accepted_mime_type?(request, accepted_mime_types, configuration)
168
- mime_type = request.env[HTTP_CONTENT_TYPE] || default_content_type(configuration) || DEFAULT_CONTENT_TYPE
181
+ mime_type = request.env[Action::HTTP_CONTENT_TYPE] ||
182
+ default_content_type(configuration) ||
183
+ Action::DEFAULT_CONTENT_TYPE
169
184
 
170
185
  !accepted_mime_types.find { |mt| ::Rack::Mime.match?(mt, mime_type) }.nil?
171
186
  end
@@ -175,6 +190,9 @@ module Hanami
175
190
  # @see Hanami::Action::Mime#call
176
191
  #
177
192
  # @return [String]
193
+ #
194
+ # @since 2.0.0
195
+ # @api private
178
196
  def self.calculate_content_type_with_charset(configuration, request, accepted_mime_types)
179
197
  charset = self.charset(configuration.default_charset)
180
198
  content_type = self.content_type(configuration, request, accepted_mime_types)
@@ -197,6 +215,7 @@ module Hanami
197
215
  ::Rack::Utils.q_values(q_value_header).each_with_index.map do |(req_mime, quality), index|
198
216
  match = available_mimes.find { |am| ::Rack::Mime.match?(am, req_mime) }
199
217
  next unless match
218
+
200
219
  RequestMimeWeight.new(req_mime, quality, index, match)
201
220
  end.compact.max&.format
202
221
  end
@@ -204,6 +223,16 @@ module Hanami
204
223
  # @since 1.0.1
205
224
  # @api private
206
225
  class RequestMimeWeight
226
+ # @since 2.0.0
227
+ # @api private
228
+ MIME_SEPARATOR = "/"
229
+ private_constant :MIME_SEPARATOR
230
+
231
+ # @since 2.0.0
232
+ # @api private
233
+ MIME_WILDCARD = "*"
234
+ private_constant :MIME_WILDCARD
235
+
207
236
  include Comparable
208
237
 
209
238
  # @since 1.0.1
@@ -237,6 +266,7 @@ module Hanami
237
266
  # @api private
238
267
  def <=>(other)
239
268
  return priority <=> other.priority unless priority == other.priority
269
+
240
270
  other.index <=> index
241
271
  end
242
272
 
@@ -245,7 +275,7 @@ module Hanami
245
275
  # @since 1.0.1
246
276
  # @api private
247
277
  def calculate_priority(mime)
248
- @priority ||= (mime.split('/'.freeze, 2).count('*'.freeze) * -10) + quality
278
+ @priority ||= (mime.split(MIME_SEPARATOR, 2).count(MIME_WILDCARD) * -10) + quality
249
279
  end
250
280
  end
251
281
  end