otto 1.2.0 → 1.3.0

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,11 +1,10 @@
1
1
  # lib/otto/design_system.rb
2
2
 
3
3
  class Otto
4
+ # Shared design system for Otto framework examples
5
+ # Provides consistent styling, components, and utilities
4
6
  module DesignSystem
5
- # Shared design system for Otto framework examples
6
- # Provides consistent styling, components, and utilities
7
-
8
- def otto_page(content, title = "Otto Framework", additional_head = "")
7
+ def otto_page(content, title = 'Otto Framework', additional_head = '')
9
8
  <<~HTML
10
9
  <!DOCTYPE html>
11
10
  <html lang="en">
@@ -25,9 +24,9 @@ class Otto
25
24
  HTML
26
25
  end
27
26
 
28
- def otto_input(name, type: "text", placeholder: "", value: "", required: false)
29
- req_attr = required ? "required" : ""
30
- val_attr = value.empty? ? "" : %{value="#{escape_html(value)}"}
27
+ def otto_input(name, type: 'text', placeholder: '', value: '', required: false)
28
+ req_attr = required ? 'required' : ''
29
+ val_attr = value.empty? ? '' : %(value="#{escape_html(value)}")
31
30
 
32
31
  <<~HTML
33
32
  <input
@@ -41,8 +40,8 @@ class Otto
41
40
  HTML
42
41
  end
43
42
 
44
- def otto_textarea(name, placeholder: "", value: "", rows: 4, required: false)
45
- req_attr = required ? "required" : ""
43
+ def otto_textarea(name, placeholder: '', value: '', rows: 4, required: false)
44
+ req_attr = required ? 'required' : ''
46
45
 
47
46
  <<~HTML
48
47
  <textarea
@@ -55,8 +54,8 @@ class Otto
55
54
  HTML
56
55
  end
57
56
 
58
- def otto_button(text, type: "submit", variant: "primary", size: "default")
59
- size_class = size == "small" ? "otto-btn-sm" : ""
57
+ def otto_button(text, type: 'submit', variant: 'primary', size: 'default')
58
+ size_class = size == 'small' ? 'otto-btn-sm' : ''
60
59
 
61
60
  <<~HTML
62
61
  <button type="#{type}" class="otto-btn otto-btn-#{variant} #{size_class}">
@@ -66,7 +65,7 @@ class Otto
66
65
  end
67
66
 
68
67
  def otto_alert(type, title, message, dismissible: false)
69
- dismiss_btn = dismissible ? '<button class="otto-alert-dismiss" onclick="this.parentElement.remove()">×</button>' : ""
68
+ dismiss_btn = dismissible ? '<button class="otto-alert-dismiss" onclick="this.parentElement.remove()">×</button>' : ''
70
69
 
71
70
  <<~HTML
72
71
  <div class="otto-alert otto-alert-#{type}">
@@ -77,9 +76,9 @@ class Otto
77
76
  HTML
78
77
  end
79
78
 
80
- def otto_card(title = nil, &block)
81
- content = block_given? ? yield : ""
82
- title_html = title ? "<h2 class=\"otto-card-title\">#{escape_html(title)}</h2>" : ""
79
+ def otto_card(title = nil, &)
80
+ content = block_given? ? yield : ''
81
+ title_html = title ? "<h2 class=\"otto-card-title\">#{escape_html(title)}</h2>" : ''
83
82
 
84
83
  <<~HTML
85
84
  <div class="otto-card">
@@ -90,7 +89,7 @@ class Otto
90
89
  end
91
90
 
92
91
  def otto_link(text, href, external: false)
93
- target_attr = external ? 'target="_blank" rel="noopener noreferrer"' : ""
92
+ target_attr = external ? 'target="_blank" rel="noopener noreferrer"' : ''
94
93
 
95
94
  <<~HTML
96
95
  <a href="#{escape_html(href)}" class="otto-link" #{target_attr}>
@@ -99,7 +98,7 @@ class Otto
99
98
  HTML
100
99
  end
101
100
 
102
- def otto_code_block(code, language = "")
101
+ def otto_code_block(code, language = '')
103
102
  <<~HTML
104
103
  <div class="otto-code-block">
105
104
  <pre><code class="language-#{language}">#{escape_html(code)}</code></pre>
@@ -111,12 +110,13 @@ class Otto
111
110
 
112
111
  def escape_html(text)
113
112
  return '' if text.nil?
113
+
114
114
  text.to_s
115
- .gsub('&', '&amp;')
116
- .gsub('<', '&lt;')
117
- .gsub('>', '&gt;')
118
- .gsub('"', '&quot;')
119
- .gsub("'", '&#x27;')
115
+ .gsub('&', '&amp;')
116
+ .gsub('<', '&lt;')
117
+ .gsub('>', '&gt;')
118
+ .gsub('"', '&quot;')
119
+ .gsub("'", '&#x27;')
120
120
  end
121
121
 
122
122
  def otto_styles
@@ -18,7 +18,7 @@ class Otto
18
18
  forwarded_ips = [
19
19
  env['HTTP_X_FORWARDED_FOR'],
20
20
  env['HTTP_X_REAL_IP'],
21
- env['HTTP_CLIENT_IP']
21
+ env['HTTP_CLIENT_IP'],
22
22
  ].compact.map { |header| header.split(/,\s*/) }.flatten
23
23
 
24
24
  # Return the first valid IP that's not a private/loopback address
@@ -115,8 +115,6 @@ class Otto
115
115
  otto.security_config
116
116
  elsif defined?(Otto) && Otto.respond_to?(:security_config)
117
117
  Otto.security_config
118
- else
119
- nil
120
118
  end
121
119
  end
122
120
 
@@ -153,7 +151,7 @@ class Otto
153
151
  /\A192\.168\./, # 192.168.0.0/16
154
152
  /\A169\.254\./, # 169.254.0.0/16 (link-local)
155
153
  /\A224\./, # 224.0.0.0/4 (multicast)
156
- /\A0\./ # 0.0.0.0/8
154
+ /\A0\./, # 0.0.0.0/8
157
155
  ]
158
156
 
159
157
  private_ranges.any? { |range| ip.match?(range) }
@@ -163,7 +161,7 @@ class Otto
163
161
  return false unless ip
164
162
 
165
163
  # Check for localhost
166
- return true if ip == '127.0.0.1' || ip == '::1'
164
+ return true if ['127.0.0.1', '::1'].include?(ip)
167
165
 
168
166
  # Check for private IP ranges
169
167
  private_ip?(ip)
@@ -5,72 +5,46 @@ class Otto
5
5
  attr_accessor :request
6
6
 
7
7
  def send_secure_cookie(name, value, ttl, opts = {})
8
- send_cookie name, value, ttl, opts.merge(secure: true)
9
- end
10
-
11
- def send_cookie(name, value, ttl, opts = {})
12
8
  # Default security options
13
9
  defaults = {
14
10
  secure: true,
15
11
  httponly: true,
16
- samesite: :lax,
17
- path: '/'
12
+ same_site: :strict,
13
+ path: '/',
18
14
  }
19
15
 
20
16
  # Merge with provided options
21
17
  cookie_opts = defaults.merge(opts)
22
18
 
23
- # Adjust secure flag for local development
24
- if request.local?
25
- cookie_opts[:secure] = false
26
- end
27
-
28
19
  # Set expiration using max-age (preferred) and expires (fallback)
29
- if ttl && ttl > 0
20
+ if ttl&.positive?
30
21
  cookie_opts[:max_age] = ttl
31
22
  cookie_opts[:expires] = (Time.now.utc + ttl + 10)
32
- elsif ttl && ttl < 0
23
+ elsif ttl&.negative?
33
24
  # For deletion, set both to past date
34
25
  cookie_opts[:max_age] = 0
35
- cookie_opts[:expires] = Time.now.utc - 86400
26
+ cookie_opts[:expires] = Time.now.utc - 86_400
36
27
  end
37
28
 
38
29
  # Set the cookie value
39
30
  cookie_opts[:value] = value
40
31
 
41
32
  # Validate SameSite attribute
42
- valid_samesite = [:strict, :lax, :none, 'Strict', 'Lax', 'None']
43
- unless valid_samesite.include?(cookie_opts[:samesite])
44
- cookie_opts[:samesite] = :lax
45
- end
33
+ valid_same_site = [:strict, :lax, :none, 'Strict', 'Lax', 'None']
34
+ cookie_opts[:same_site] = :strict unless valid_same_site.include?(cookie_opts[:same_site])
46
35
 
47
36
  # If SameSite=None, Secure must be true
48
- if cookie_opts[:samesite].to_s.downcase == 'none'
49
- cookie_opts[:secure] = true
50
- end
37
+ cookie_opts[:secure] = true if cookie_opts[:same_site].to_s.downcase == 'none'
51
38
 
52
39
  set_cookie name, cookie_opts
53
40
  end
54
41
 
55
- def delete_cookie(name, opts = {})
56
- # Ensure we use the same path and domain when deleting
57
- delete_opts = {
58
- path: opts[:path] || '/',
59
- domain: opts[:domain],
60
- secure: opts[:secure],
61
- httponly: opts[:httponly],
62
- samesite: opts[:samesite]
63
- }.compact
64
-
65
- send_cookie name, '', -1, delete_opts
66
- end
67
-
68
42
  def send_session_cookie(name, value, opts = {})
69
43
  # Session cookies don't have expiration
70
44
  session_opts = opts.merge(
71
45
  secure: true,
72
46
  httponly: true,
73
- samesite: :lax
47
+ samesite: :strict,
74
48
  )
75
49
 
76
50
  # Remove expiration-related options for session cookies
@@ -78,9 +52,7 @@ class Otto
78
52
  session_opts.delete(:expires)
79
53
 
80
54
  # Adjust secure flag for local development
81
- if request.local?
82
- session_opts[:secure] = false
83
- end
55
+ session_opts[:secure] = false if request.local?
84
56
 
85
57
  session_opts[:value] = value
86
58
  set_cookie name, session_opts
data/lib/otto/route.rb CHANGED
@@ -33,20 +33,20 @@ class Otto
33
33
  # @param definition [String] Class and method definition (Class.method or Class#method)
34
34
  # @raise [ArgumentError] if definition format is invalid or class name is unsafe
35
35
  def initialize(verb, path, definition)
36
- @verb = verb.to_s.upcase.to_sym
37
- @path = path
38
- @definition = definition
36
+ @verb = verb.to_s.upcase.to_sym
37
+ @path = path
38
+ @definition = definition
39
39
  @pattern, @keys = *compile(@path)
40
40
  if !@definition.index('.').nil?
41
41
  @klass, @name = @definition.split('.')
42
- @kind = :class
42
+ @kind = :class
43
43
  elsif !@definition.index('#').nil?
44
44
  @klass, @name = @definition.split('#')
45
- @kind = :instance
45
+ @kind = :instance
46
46
  else
47
47
  raise ArgumentError, "Bad definition: #{@definition}"
48
48
  end
49
- @klass = safe_const_get(@klass)
49
+ @klass = safe_const_get(@klass)
50
50
  # @method = @klass.method(@name)
51
51
  end
52
52
 
@@ -85,8 +85,8 @@ class Otto
85
85
 
86
86
  begin
87
87
  Object.const_get(class_name)
88
- rescue NameError => e
89
- raise ArgumentError, "Class not found: #{class_name} - #{e.message}"
88
+ rescue NameError => ex
89
+ raise ArgumentError, "Class not found: #{class_name} - #{ex.message}"
90
90
  end
91
91
  end
92
92
 
@@ -109,11 +109,11 @@ class Otto
109
109
  # @return [Array] Rack response array [status, headers, body]
110
110
  def call(env, extra_params = {})
111
111
  extra_params ||= {}
112
- req = Rack::Request.new(env)
113
- res = Rack::Response.new
112
+ req = Rack::Request.new(env)
113
+ res = Rack::Response.new
114
114
  req.extend Otto::RequestHelpers
115
115
  res.extend Otto::ResponseHelpers
116
- res.request = req
116
+ res.request = req
117
117
 
118
118
  # Process parameters through security layer
119
119
  req.params.merge! extra_params
@@ -158,7 +158,7 @@ class Otto
158
158
  keys = []
159
159
  if path.respond_to? :to_str
160
160
  special_chars = %w[. + ( ) $]
161
- pattern =
161
+ pattern =
162
162
  path.to_str.gsub(/((:\w+)|[\*#{special_chars.join}])/) do |match|
163
163
  case match
164
164
  when '*'
@@ -22,26 +22,26 @@ class Otto
22
22
  # config.max_param_depth = 16
23
23
  class Config
24
24
  attr_accessor :csrf_protection, :csrf_token_key, :csrf_header_key, :csrf_session_key,
25
- :max_request_size, :max_param_depth, :max_param_keys,
26
- :trusted_proxies, :require_secure_cookies,
27
- :security_headers, :input_validation
25
+ :max_request_size, :max_param_depth, :max_param_keys,
26
+ :trusted_proxies, :require_secure_cookies,
27
+ :security_headers, :input_validation
28
28
 
29
29
  # Initialize security configuration with safe defaults
30
30
  #
31
31
  # All security features are disabled by default to maintain backward
32
32
  # compatibility with existing Otto applications.
33
33
  def initialize
34
- @csrf_protection = false
35
- @csrf_token_key = '_csrf_token'
36
- @csrf_header_key = 'HTTP_X_CSRF_TOKEN'
37
- @csrf_session_key = '_csrf_session_id'
38
- @max_request_size = 10 * 1024 * 1024 # 10MB
39
- @max_param_depth = 32
40
- @max_param_keys = 64
41
- @trusted_proxies = []
34
+ @csrf_protection = false
35
+ @csrf_token_key = '_csrf_token'
36
+ @csrf_header_key = 'HTTP_X_CSRF_TOKEN'
37
+ @csrf_session_key = '_csrf_session_id'
38
+ @max_request_size = 10 * 1024 * 1024 # 10MB
39
+ @max_param_depth = 32
40
+ @max_param_keys = 64
41
+ @trusted_proxies = []
42
42
  @require_secure_cookies = false
43
- @security_headers = default_security_headers
44
- @input_validation = true
43
+ @security_headers = default_security_headers
44
+ @input_validation = true
45
45
  end
46
46
 
47
47
  # Enable CSRF (Cross-Site Request Forgery) protection
@@ -96,7 +96,7 @@ class Otto
96
96
  when Array
97
97
  @trusted_proxies.concat(proxy)
98
98
  else
99
- raise ArgumentError, "Proxy must be a String or Array"
99
+ raise ArgumentError, 'Proxy must be a String or Array'
100
100
  end
101
101
  end
102
102
 
@@ -130,19 +130,19 @@ class Otto
130
130
  size = content_length.to_i
131
131
  if size > @max_request_size
132
132
  raise Otto::Security::RequestTooLargeError,
133
- "Request size #{size} exceeds maximum #{@max_request_size}"
133
+ "Request size #{size} exceeds maximum #{@max_request_size}"
134
134
  end
135
135
  true
136
136
  end
137
137
 
138
138
  def generate_csrf_token(session_id = nil)
139
- base = session_id || 'no-session'
140
- token = SecureRandom.hex(32)
139
+ base = session_id || 'no-session'
140
+ token = SecureRandom.hex(32)
141
141
  hash_input = base + ':' + token
142
- signature = Digest::SHA256.hexdigest(hash_input)
142
+ signature = Digest::SHA256.hexdigest(hash_input)
143
143
  csrf_token = "#{token}:#{signature}"
144
144
 
145
- puts "=== CSRF Generation ==="
145
+ puts '=== CSRF Generation ==='
146
146
  puts "hash_input: #{hash_input.inspect}"
147
147
  puts "signature: #{signature}"
148
148
  puts "csrf_token: #{csrf_token}"
@@ -156,12 +156,12 @@ class Otto
156
156
  token_part, signature = token.split(':')
157
157
  return false if token_part.nil? || signature.nil?
158
158
 
159
- base = session_id || 'no-session'
160
- hash_input = "#{base}:#{token_part}"
159
+ base = session_id || 'no-session'
160
+ hash_input = "#{base}:#{token_part}"
161
161
  expected_signature = Digest::SHA256.hexdigest(hash_input)
162
- comparison_result = secure_compare(signature, expected_signature)
162
+ comparison_result = secure_compare(signature, expected_signature)
163
163
 
164
- puts "=== CSRF Verification ==="
164
+ puts '=== CSRF Verification ==='
165
165
  puts "hash_input: #{hash_input.inspect}"
166
166
  puts "received_signature: #{signature}"
167
167
  puts "expected_signature: #{expected_signature}"
@@ -179,9 +179,9 @@ class Otto
179
179
  # @param max_age [Integer] Maximum age in seconds (default: 1 year)
180
180
  # @param include_subdomains [Boolean] Apply to all subdomains (default: true)
181
181
  # @return [void]
182
- def enable_hsts!(max_age: 31536000, include_subdomains: true)
183
- hsts_value = "max-age=#{max_age}"
184
- hsts_value += "; includeSubDomains" if include_subdomains
182
+ def enable_hsts!(max_age: 31_536_000, include_subdomains: true)
183
+ hsts_value = "max-age=#{max_age}"
184
+ hsts_value += '; includeSubDomains' if include_subdomains
185
185
  @security_headers['strict-transport-security'] = hsts_value
186
186
  end
187
187
 
@@ -245,27 +245,23 @@ class Otto
245
245
  return session[csrf_session_key] if session[csrf_session_key]
246
246
  return session['session_id'] if session['session_id']
247
247
  end
248
- rescue
248
+ rescue StandardError
249
249
  # Fall through to cookies
250
250
  end
251
251
 
252
252
  # Try cookies
253
253
  request.cookies['_otto_session'] ||
254
- request.cookies['session_id'] ||
255
- request.cookies['_session_id']
254
+ request.cookies['session_id'] ||
255
+ request.cookies['_session_id']
256
256
  end
257
257
 
258
258
  def store_session_id(request, session_id)
259
- begin
260
- session = request.session
259
+ session = request.session
261
260
  session[csrf_session_key] = session_id if session
262
- rescue
263
- # Cookie fallback handled in inject_csrf_token
264
- end
261
+ rescue StandardError
262
+ # Cookie fallback handled in inject_csrf_token
265
263
  end
266
264
 
267
- private
268
-
269
265
  # Default security headers applied to all responses
270
266
  #
271
267
  # These headers provide basic defense against common web vulnerabilities:
@@ -282,7 +278,7 @@ class Otto
282
278
  {
283
279
  'x-content-type-options' => 'nosniff',
284
280
  'x-xss-protection' => '1; mode=block',
285
- 'referrer-policy' => 'strict-origin-when-cross-origin'
281
+ 'referrer-policy' => 'strict-origin-when-cross-origin',
286
282
  }
287
283
  end
288
284
 
@@ -298,7 +294,7 @@ class Otto
298
294
  def secure_compare(a, b)
299
295
  return false if a.nil? || b.nil? || a.length != b.length
300
296
 
301
- result = 0
297
+ result = 0
302
298
  a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
303
299
  result == 0
304
300
  end
@@ -3,12 +3,14 @@
3
3
  require 'securerandom'
4
4
 
5
5
  class Otto
6
+ # CSRF protection middleware for Otto framework
6
7
  module Security
8
+ # Middleware that provides Cross-Site Request Forgery (CSRF) protection
7
9
  class CSRFMiddleware
8
10
  SAFE_METHODS = %w[GET HEAD OPTIONS TRACE].freeze
9
11
 
10
12
  def initialize(app, config = nil)
11
- @app = app
13
+ @app = app
12
14
  @config = config || Otto::Security::Config.new
13
15
  end
14
16
 
@@ -25,9 +27,7 @@ class Otto
25
27
  end
26
28
 
27
29
  # Validate CSRF token for unsafe methods
28
- unless valid_csrf_token?(request)
29
- return csrf_error_response
30
- end
30
+ return csrf_error_response unless valid_csrf_token?(request)
31
31
 
32
32
  @app.call(env)
33
33
  end
@@ -54,8 +54,7 @@ class Otto
54
54
  token ||= request.env[@config.csrf_header_key]
55
55
 
56
56
  # Try alternative header format
57
- token ||= request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' ?
58
- request.env['HTTP_X_CSRF_TOKEN'] : nil
57
+ token ||= request.env['HTTP_X_CSRF_TOKEN'] if request.env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
59
58
 
60
59
  token
61
60
  end
@@ -68,7 +67,7 @@ class Otto
68
67
  return response unless response.is_a?(Array) && response.length >= 3
69
68
 
70
69
  status, headers, body = response
71
- content_type = headers.find { |k, v| k.downcase == 'content-type' }&.last
70
+ content_type = headers.find { |k, _v| k.downcase == 'content-type' }&.last
72
71
 
73
72
  return response unless content_type&.include?('text/html')
74
73
 
@@ -85,14 +84,12 @@ class Otto
85
84
  body_content = body.respond_to?(:join) ? body.join : body.to_s
86
85
 
87
86
  if body_content.match?(/<head>/i)
88
- meta_tag = %(<meta name="csrf-token" content="#{csrf_token}">)
87
+ meta_tag = %(<meta name="csrf-token" content="#{csrf_token}">)
89
88
  body_content = body_content.sub(/<head>/i, "<head>\n#{meta_tag}")
90
89
 
91
90
  # Update content length if present
92
- content_length_key = headers.keys.find { |k| k.downcase == 'content-length' }
93
- if content_length_key
94
- headers[content_length_key] = body_content.bytesize.to_s
95
- end
91
+ content_length_key = headers.keys.find { |k| k.downcase == 'content-length' }
92
+ headers[content_length_key] = body_content.bytesize.to_s if content_length_key
96
93
 
97
94
  [status, headers, [body_content]]
98
95
  else
@@ -106,8 +103,8 @@ class Otto
106
103
  return if existing_cookie == session_id
107
104
 
108
105
  # Set the session cookie
109
- cookie_value = "#{session_id}; Path=/; HttpOnly; SameSite=Lax"
110
- cookie_value += "; Secure" if request.scheme == 'https'
106
+ cookie_value = "#{session_id}; Path=/; HttpOnly; SameSite=Lax"
107
+ cookie_value += '; Secure' if request.scheme == 'https'
111
108
 
112
109
  # Handle existing Set-Cookie headers
113
110
  existing_cookies = headers['set-cookie'] || headers['Set-Cookie']
@@ -126,8 +123,8 @@ class Otto
126
123
  def html_response?(response)
127
124
  return false unless response.is_a?(Array) && response.length >= 2
128
125
 
129
- headers = response[1]
130
- content_type = headers.find { |k, v| k.downcase == 'content-type' }&.last
126
+ headers = response[1]
127
+ content_type = headers.find { |k, _v| k.downcase == 'content-type' }&.last
131
128
  content_type&.include?('text/html')
132
129
  end
133
130
 
@@ -136,34 +133,30 @@ class Otto
136
133
  403,
137
134
  {
138
135
  'content-type' => 'application/json',
139
- 'content-length' => csrf_error_body.bytesize.to_s
136
+ 'content-length' => csrf_error_body.bytesize.to_s,
140
137
  },
141
- [csrf_error_body]
138
+ [csrf_error_body],
142
139
  ]
143
140
  end
144
141
 
145
142
  def csrf_error_body
146
143
  {
147
144
  error: 'CSRF token validation failed',
148
- message: 'The request could not be authenticated. Please refresh the page and try again.'
145
+ message: 'The request could not be authenticated. Please refresh the page and try again.',
149
146
  }.to_json
150
147
  end
151
-
152
148
  end
153
149
 
150
+ # Helper methods for CSRF token handling in views and controllers
154
151
  module CSRFHelpers
155
152
  def csrf_token
156
153
  if @csrf_token.nil? && otto.respond_to?(:security_config)
157
- session_id = otto.security_config.get_or_create_session_id(req)
154
+ session_id = otto.security_config.get_or_create_session_id(req)
158
155
  @csrf_token = otto.security_config.generate_csrf_token(session_id)
159
156
  end
160
157
  @csrf_token
161
158
  end
162
159
 
163
- private
164
-
165
- public
166
-
167
160
  def csrf_meta_tag
168
161
  %(<meta name="csrf-token" content="#{csrf_token}">)
169
162
  end
@@ -173,8 +166,11 @@ class Otto
173
166
  end
174
167
 
175
168
  def csrf_token_key
176
- otto.respond_to?(:security_config) ?
177
- otto.security_config.csrf_token_key : '_csrf_token'
169
+ if otto.respond_to?(:security_config)
170
+ otto.security_config.csrf_token_key
171
+ else
172
+ '_csrf_token'
173
+ end
178
174
  end
179
175
  end
180
176
  end