hanami-controller 2.0.0.alpha8 → 2.0.0.beta1

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.
@@ -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