hanami-controller 1.3.3 → 2.0.0.alpha4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +299 -537
  5. data/hanami-controller.gemspec +4 -3
  6. data/lib/hanami/action/application_action.rb +131 -0
  7. data/lib/hanami/action/application_configuration/content_security_policy.rb +118 -0
  8. data/lib/hanami/action/application_configuration/cookies.rb +29 -0
  9. data/lib/hanami/action/application_configuration/sessions.rb +46 -0
  10. data/lib/hanami/action/application_configuration.rb +90 -0
  11. data/lib/hanami/action/base_params.rb +2 -2
  12. data/lib/hanami/action/cache/cache_control.rb +4 -4
  13. data/lib/hanami/action/cache/conditional_get.rb +3 -1
  14. data/lib/hanami/action/cache/directives.rb +1 -1
  15. data/lib/hanami/action/cache/expires.rb +3 -3
  16. data/lib/hanami/action/cache.rb +1 -139
  17. data/lib/hanami/action/configuration.rb +428 -0
  18. data/lib/hanami/action/cookie_jar.rb +3 -3
  19. data/lib/hanami/action/cookies.rb +3 -62
  20. data/lib/hanami/action/csrf_protection.rb +214 -0
  21. data/lib/hanami/action/flash.rb +102 -207
  22. data/lib/hanami/action/glue.rb +5 -31
  23. data/lib/hanami/action/halt.rb +12 -0
  24. data/lib/hanami/action/mime.rb +78 -485
  25. data/lib/hanami/action/params.rb +2 -2
  26. data/lib/hanami/action/rack/file.rb +1 -1
  27. data/lib/hanami/action/request.rb +30 -20
  28. data/lib/hanami/action/response.rb +193 -0
  29. data/lib/hanami/action/session.rb +11 -128
  30. data/lib/hanami/action/standalone_action.rb +578 -0
  31. data/lib/hanami/action/validatable.rb +1 -1
  32. data/lib/hanami/action/view_name_inferrer.rb +46 -0
  33. data/lib/hanami/action.rb +129 -73
  34. data/lib/hanami/controller/version.rb +1 -1
  35. data/lib/hanami/controller.rb +0 -227
  36. data/lib/hanami/http/status.rb +2 -2
  37. metadata +44 -26
  38. data/lib/hanami/action/callable.rb +0 -92
  39. data/lib/hanami/action/callbacks.rb +0 -214
  40. data/lib/hanami/action/configurable.rb +0 -50
  41. data/lib/hanami/action/exposable/guard.rb +0 -104
  42. data/lib/hanami/action/exposable.rb +0 -126
  43. data/lib/hanami/action/head.rb +0 -121
  44. data/lib/hanami/action/rack/callable.rb +0 -47
  45. data/lib/hanami/action/rack/errors.rb +0 -53
  46. data/lib/hanami/action/rack.rb +0 -411
  47. data/lib/hanami/action/redirect.rb +0 -59
  48. data/lib/hanami/action/throwable.rb +0 -169
  49. data/lib/hanami/controller/configuration.rb +0 -763
  50. data/lib/hanami-controller.rb +0 -1
@@ -1,7 +1,8 @@
1
1
  require 'rack/request'
2
+ require 'securerandom'
2
3
 
3
4
  module Hanami
4
- module Action
5
+ class Action
5
6
  # An HTTP request based on top of Rack::Request.
6
7
  # This guarantees backwards compatibility with with Rack.
7
8
  #
@@ -9,36 +10,45 @@ module Hanami
9
10
  #
10
11
  # @see http://www.rubydoc.info/gems/rack/Rack/Request
11
12
  class Request < ::Rack::Request
12
- # @raise [NotImplementedError]
13
- #
14
- # @since 0.3.1
15
- # @api private
16
- def content_type
17
- raise NotImplementedError, 'Please use Action#content_type'
13
+ HTTP_ACCEPT = "HTTP_ACCEPT".freeze
14
+ REQUEST_ID = "hanami.request_id".freeze
15
+ DEFAULT_ACCEPT = "*/*".freeze
16
+ DEFAULT_ID_LENGTH = 16
17
+
18
+ attr_reader :params
19
+
20
+ def initialize(env, params)
21
+ super(env)
22
+ @params = params
18
23
  end
19
24
 
20
- # @raise [NotImplementedError]
21
- #
22
- # @since 0.3.1
23
- # @api private
24
- def session
25
- raise NotImplementedError, 'Please include Action::Session and use Action#session'
25
+ def id
26
+ # FIXME make this number configurable and document the probabilities of clashes
27
+ @id ||= @env[REQUEST_ID] = SecureRandom.hex(DEFAULT_ID_LENGTH)
26
28
  end
27
29
 
28
- # @raise [NotImplementedError]
29
- #
30
- # @since 0.3.1
30
+ def accept?(mime_type)
31
+ !!::Rack::Utils.q_values(accept).find do |mime, _|
32
+ ::Rack::Mime.match?(mime_type, mime)
33
+ end
34
+ end
35
+
36
+ def accept_header?
37
+ accept != DEFAULT_ACCEPT
38
+ end
39
+
40
+ # @since 0.1.0
31
41
  # @api private
32
- def cookies
33
- raise NotImplementedError, 'Please include Action::Cookies and use Action#cookies'
42
+ def accept
43
+ @accept ||= @env[HTTP_ACCEPT] || DEFAULT_ACCEPT
34
44
  end
35
45
 
36
46
  # @raise [NotImplementedError]
37
47
  #
38
48
  # @since 0.3.1
39
49
  # @api private
40
- def params
41
- raise NotImplementedError, 'Please use params passed to Action#call'
50
+ def content_type
51
+ raise NotImplementedError, 'Please use Action#content_type'
42
52
  end
43
53
 
44
54
  # @raise [NotImplementedError]
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+ require 'rack/response'
5
+ require 'hanami/utils/kernel'
6
+ require 'hanami/action/flash'
7
+ require 'hanami/action/halt'
8
+ require 'hanami/action/cookie_jar'
9
+ require 'hanami/action/cache/cache_control'
10
+ require 'hanami/action/cache/expires'
11
+ require 'hanami/action/cache/conditional_get'
12
+
13
+ module Hanami
14
+ class Action
15
+ class Response < ::Rack::Response
16
+ DEFAULT_VIEW_OPTIONS = -> * { {} }.freeze
17
+
18
+ REQUEST_METHOD = "REQUEST_METHOD"
19
+ HTTP_ACCEPT = "HTTP_ACCEPT"
20
+ SESSION_KEY = "rack.session"
21
+ REQUEST_ID = "hanami.request_id"
22
+ LOCATION = "Location"
23
+
24
+ X_CASCADE = "X-Cascade"
25
+ CONTENT_LENGTH = "Content-Length"
26
+ NOT_FOUND = 404
27
+
28
+ RACK_STATUS = 0
29
+ RACK_HEADERS = 1
30
+ RACK_BODY = 2
31
+
32
+ HEAD = "HEAD"
33
+
34
+ FLASH_SESSION_KEY = "_flash"
35
+
36
+ EMPTY_BODY = [].freeze
37
+
38
+ FILE_SYSTEM_ROOT = Pathname.new("/").freeze
39
+
40
+ attr_reader :request, :action, :exposures, :format, :env, :view_options
41
+ attr_accessor :charset
42
+
43
+ def self.build(status, env)
44
+ new(action: "", configuration: nil, content_type: Mime.best_q_match(env[HTTP_ACCEPT]), env: env).tap do |r|
45
+ r.status = status
46
+ r.body = Http::Status.message_for(status)
47
+ r.set_format(Mime.format_for(r.content_type))
48
+ end
49
+ end
50
+
51
+ def initialize(request:, action:, configuration:, content_type: nil, env: {}, headers: {}, view_options: nil)
52
+ super([], 200, headers.dup)
53
+ set_header("Content-Type", content_type)
54
+
55
+ @request = request
56
+ @action = action
57
+ @configuration = configuration
58
+ @charset = ::Rack::MediaType.params(content_type).fetch('charset', nil)
59
+ @exposures = {}
60
+ @env = env
61
+ @view_options = view_options || DEFAULT_VIEW_OPTIONS
62
+
63
+ @sending_file = false
64
+ end
65
+
66
+ def body=(str)
67
+ @length = 0
68
+ @body = EMPTY_BODY.dup
69
+
70
+ # FIXME: there could be a bug that prevents Content-Length to be sent for files
71
+ if str.is_a?(::Rack::File::Iterator)
72
+ @body = str
73
+ else
74
+ write(str) unless str.nil? || str == EMPTY_BODY
75
+ end
76
+ end
77
+
78
+ def render(view, **options)
79
+ self.body = view.(**view_options.(request, self), **exposures.merge(options)).to_str
80
+ end
81
+
82
+ def format=(args)
83
+ @format, content_type = *args
84
+ content_type = Action::Mime.content_type_with_charset(content_type, charset)
85
+ set_header("Content-Type", content_type)
86
+ end
87
+
88
+ def [](key)
89
+ @exposures.fetch(key)
90
+ end
91
+
92
+ def []=(key, value)
93
+ @exposures[key] = value
94
+ end
95
+
96
+ def session
97
+ env[SESSION_KEY] ||= {}
98
+ end
99
+
100
+ def cookies
101
+ @cookies ||= CookieJar.new(env.dup, headers, @configuration.cookies)
102
+ end
103
+
104
+ def flash
105
+ @flash ||= Flash.new(session[FLASH_SESSION_KEY])
106
+ end
107
+
108
+ def redirect_to(url, status: 302)
109
+ return unless renderable?
110
+
111
+ redirect(::String.new(url), status)
112
+ Halt.call(status)
113
+ end
114
+
115
+ def send_file(path)
116
+ _send_file(
117
+ Rack::File.new(path, @configuration.public_directory).call(env)
118
+ )
119
+ end
120
+
121
+ def unsafe_send_file(path)
122
+ directory = if Pathname.new(path).relative?
123
+ @configuration.root_directory
124
+ else
125
+ FILE_SYSTEM_ROOT
126
+ end
127
+
128
+ _send_file(
129
+ Rack::File.new(path, directory).call(env)
130
+ )
131
+ end
132
+
133
+ def cache_control(*values)
134
+ directives = Cache::CacheControl::Directives.new(*values)
135
+ headers.merge!(directives.headers)
136
+ end
137
+
138
+ def expires(amount, *values)
139
+ directives = Cache::Expires::Directives.new(amount, *values)
140
+ headers.merge!(directives.headers)
141
+ end
142
+
143
+ def fresh(options)
144
+ conditional_get = Cache::ConditionalGet.new(env, options)
145
+
146
+ headers.merge!(conditional_get.headers)
147
+
148
+ conditional_get.fresh? do
149
+ Halt.call(304)
150
+ end
151
+ end
152
+
153
+ # @api private
154
+ def request_id
155
+ env.fetch(REQUEST_ID) do
156
+ # FIXME: raise a meaningful error, by inviting devs to include Hanami::Action::Session
157
+ raise "Can't find request ID"
158
+ end
159
+ end
160
+
161
+ def set_format(value)
162
+ @format = value
163
+ end
164
+
165
+ def renderable?
166
+ return !head? && body.empty? if body.respond_to?(:empty?)
167
+
168
+ !@sending_file && !head?
169
+ end
170
+
171
+ alias to_ary to_a
172
+
173
+ def head?
174
+ env[REQUEST_METHOD] == HEAD
175
+ end
176
+
177
+ # @api private
178
+ def _send_file(send_file_response)
179
+ headers.merge!(send_file_response[RACK_HEADERS])
180
+
181
+ if send_file_response[RACK_STATUS] == NOT_FOUND
182
+ headers.delete(X_CASCADE)
183
+ headers.delete(CONTENT_LENGTH)
184
+ Halt.call(NOT_FOUND)
185
+ else
186
+ self.status = send_file_response[RACK_STATUS]
187
+ self.body = send_file_response[RACK_BODY]
188
+ @sending_file = true
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
@@ -1,143 +1,21 @@
1
1
  require 'hanami/action/flash'
2
2
 
3
3
  module Hanami
4
- module Action
4
+ class Action
5
5
  # Session API
6
6
  #
7
7
  # This module isn't included by default.
8
8
  #
9
9
  # @since 0.1.0
10
10
  module Session
11
- # The key that returns raw session from the Rack env
12
- #
13
- # @since 0.1.0
14
- # @api private
15
- SESSION_KEY = 'rack.session'.freeze
16
-
17
- # The key that is used by flash to transport errors
18
- #
19
- # @since 0.3.0
20
- # @api private
21
- ERRORS_KEY = :__errors
22
-
23
- # Add session to default exposures
24
- #
25
- # @since 0.4.4
26
- # @api private
27
- def self.included(action)
28
- action.class_eval do
29
- _expose :session, :flash
11
+ def self.included(base)
12
+ base.class_eval do
13
+ before { |req, _| req.id }
30
14
  end
31
15
  end
32
16
 
33
- # Gets the session from the request and expose it as an Hash.
34
- #
35
- # @return [Hash] the HTTP session from the request
36
- #
37
- # @since 0.1.0
38
- #
39
- # @example
40
- # require 'hanami/controller'
41
- # require 'hanami/action/session'
42
- #
43
- # class Show
44
- # include Hanami::Action
45
- # include Hanami::Action::Session
46
- #
47
- # def call(params)
48
- # # ...
49
- #
50
- # # get a value
51
- # session[:user_id] # => '23'
52
- #
53
- # # set a value
54
- # session[:foo] = 'bar'
55
- #
56
- # # remove a value
57
- # session[:bax] = nil
58
- # end
59
- # end
60
- def session
61
- @_env[SESSION_KEY] ||= {}
62
- end
63
-
64
- # Read errors from flash or delegate to the superclass
65
- #
66
- # @return [Hanami::Validations::Errors] A collection of validation errors
67
- #
68
- # @since 0.3.0
69
- #
70
- # @see Hanami::Action::Validatable
71
- # @see Hanami::Action::Session#flash
72
- def errors
73
- flash[ERRORS_KEY] || params.respond_to?(:errors) && params.errors
74
- end
75
-
76
17
  private
77
18
 
78
- # Container useful to transport data with the HTTP session
79
- #
80
- # @return [Hanami::Action::Flash] a Flash instance
81
- #
82
- # @since 0.3.0
83
- #
84
- # @see Hanami::Action::Flash
85
- def flash
86
- @flash ||= Flash.new(session)
87
- end
88
-
89
- # In case of validations errors, preserve those informations after a
90
- # redirect.
91
- #
92
- # @return [void]
93
- #
94
- # @since 0.3.0
95
- # @api private
96
- #
97
- # @see Hanami::Action::Redirect#redirect_to
98
- #
99
- # @example
100
- # require 'hanami/controller'
101
- #
102
- # module Comments
103
- # class Index
104
- # include Hanami::Action
105
- # include Hanami::Action::Session
106
- #
107
- # expose :comments
108
- #
109
- # def call(params)
110
- # @comments = CommentRepository.all
111
- # end
112
- # end
113
- #
114
- # class Create
115
- # include Hanami::Action
116
- # include Hanami::Action::Session
117
- #
118
- # params do
119
- # param :text, type: String, presence: true
120
- # end
121
- #
122
- # def call(params)
123
- # comment = Comment.new(params)
124
- # CommentRepository.create(comment) if params.valid?
125
- #
126
- # redirect_to '/comments'
127
- # end
128
- # end
129
- # end
130
- #
131
- # # The validation errors caused by Comments::Create are available
132
- # # **after the redirect** in the context of Comments::Index.
133
- def redirect_to(*args)
134
- if params.respond_to?(:valid?)
135
- flash[ERRORS_KEY] = errors.to_a unless params.valid?
136
- end
137
- flash.keep!
138
- super
139
- end
140
-
141
19
  # Finalize the response
142
20
  #
143
21
  # @return [void]
@@ -146,9 +24,14 @@ module Hanami
146
24
  # @api private
147
25
  #
148
26
  # @see Hanami::Action#finish
149
- def finish
27
+ def finish(req, res, *)
28
+ if (next_flash = res.flash.next).any?
29
+ res.session['_flash'] = next_flash
30
+ else
31
+ res.session.delete('_flash')
32
+ end
33
+
150
34
  super
151
- flash.clear
152
35
  end
153
36
  end
154
37
  end