bullet 7.0.2 → 7.0.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9190b7377e0184967fec123b7926cc2287755989ceea1a4c90a949721d29f411
4
- data.tar.gz: 9ae038298d2367a5862eeb6480565f9ac9d104887b97f417e390266e0bfbdb8e
3
+ metadata.gz: 0f47e147f5df074217ee7a65123f54a1918f98bde162c7d4bf1f76bffc9f15a6
4
+ data.tar.gz: e05e48b96a5e68bfbcac63f4dce4601d717ec1a32ee83a5c45aad738e1bfc48b
5
5
  SHA512:
6
- metadata.gz: 5b8679cc1801319b7527c472be94d75a5df7a3e03a16f2ef444d3708de584f1f8985ceedd079c35c43cc9ffb39ee5a1664b5c12d38d3d194a686692495873612
7
- data.tar.gz: 6309d7925415f5a6339ab4e180a6ef0cd3d27c3308f3a39f6bbfa120c4cb0825f6c2728365942f71e810cb967504b72ad6fa2893334adb4ce6597299cbc4abbd
6
+ metadata.gz: 87bf095754befedb8f1137ae039191807baa1f1bcadb09c923b4f1a6abd385e73e4143e50077358a8f7155e3bf367edb1b405e3b0b260c8de5dd572699b7bafc
7
+ data.tar.gz: fd692ae67a1695ee4598b24d3cd030a7f091deb42f30df0f92085b3c54f765960c0b5eabaace7b306fadf8539b514fe77076cff825c27a292c5e36f56916646b
@@ -9,9 +9,9 @@ name: CI
9
9
 
10
10
  on:
11
11
  push:
12
- branches: [ master ]
12
+ branches: [ main ]
13
13
  pull_request:
14
- branches: [ master ]
14
+ branches: [ main ]
15
15
 
16
16
  jobs:
17
17
  test_rails_4:
data/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  ## Next Release
2
2
 
3
+ ## 7.0.7 (03/01/2023)
4
+
5
+ * Check `Rails.application.config.content_security_policy` before insert `Bullet::Rack`
6
+
7
+ ## 7.0.6 (03/01/2023)
8
+
9
+ * Better way to check if `ActionDispatch::ContentSecurityPolicy::Middleware` exists
10
+
11
+ ## 7.0.5 (01/01/2023)
12
+
13
+ * Fix n+1 false positives in AR 7.0
14
+ * Fix eager_load nested has_many :through false positives
15
+ * Respect Content-Security-Policy nonces
16
+ * Added CallStacks support for avoid eager loading
17
+ * Iterate fewer times over objects
18
+
19
+ ## 7.0.4 (11/28/2022)
20
+
21
+ * Fix `eager_load` `has_many :through` false positives
22
+ * mongoid7x: add dynamic methods
23
+
24
+ ## 7.0.3 (08/13/2022)
25
+
26
+ * Replace `Array()` with `Array.wrap()`
27
+
3
28
  ## 7.0.2 (05/31/2022)
4
29
 
5
30
  * Drop growl support
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 - 2010 Richard Huang (flyerhzm@gmail.com)
1
+ Copyright (c) 2009 - 2022 Richard Huang (flyerhzm@gmail.com)
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -222,7 +222,7 @@ end
222
222
 
223
223
  ### Work with sinatra
224
224
 
225
- Configure and use `Bullet::Rack`
225
+ Configure and use `Bullet::Rack`.
226
226
 
227
227
  ```ruby
228
228
  configure :development do
@@ -232,6 +232,8 @@ configure :development do
232
232
  end
233
233
  ```
234
234
 
235
+ If your application generates a Content-Security-Policy via a separate middleware, ensure that `Bullet::Rack` is loaded _before_ that middleware.
236
+
235
237
  ### Run in tests
236
238
 
237
239
  First you need to enable Bullet in test environment.
@@ -285,7 +287,7 @@ $ rails g scaffold comment name:string post_id:integer
285
287
  $ bundle exec rake db:migrate
286
288
  ```
287
289
 
288
- 2\. Change `app/model/post.rb` and `app/model/comment.rb`
290
+ 2\. Change `app/models/post.rb` and `app/models/comment.rb`
289
291
 
290
292
  ```ruby
291
293
  class Post < ActiveRecord::Base
@@ -177,16 +177,18 @@ module Bullet
177
177
  if Bullet.start?
178
178
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
179
179
  refl = reflection.through_reflection
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
181
- association = owner.association refl.name
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
180
+ association = owner.association(refl.name)
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if refl.through_reflection?
187
- refl = refl.through_reflection while refl.through_reflection?
187
+ if refl.through_reflection?
188
+ refl = refl.through_reflection while refl.through_reflection?
188
189
 
189
- Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
190
+ Bullet::Detector::NPlusOneQuery.call_association(owner, refl.name)
191
+ end
190
192
  end
191
193
  end
192
194
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -150,14 +150,16 @@ module Bullet
150
150
 
151
151
  if Bullet.start?
152
152
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
153
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
154
- association = owner.association reflection.through_reflection.name
155
- Array(association.target).each do |through_record|
156
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
157
- end
153
+ association = owner.association(reflection.through_reflection.name)
154
+ if association.loaded?
155
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
156
+ Array.wrap(association.target).each do |through_record|
157
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
158
+ end
158
159
 
159
- if reflection.through_reflection != through_reflection
160
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
160
+ if reflection.through_reflection != through_reflection
161
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
162
+ end
161
163
  end
162
164
  end
163
165
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -199,7 +201,7 @@ module Bullet
199
201
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
200
202
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
201
203
  association = owner.association reflection.through_reflection.name
202
- Array(association.target).each do |through_record|
204
+ Array.wrap(association.target).each do |through_record|
203
205
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
204
206
  end
205
207
 
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -226,7 +228,7 @@ module Bullet
226
228
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
227
229
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
228
230
  association = owner.association(reflection.through_reflection.name)
229
- Array(association.target).each do |through_record|
231
+ Array.wrap(association.target).each do |through_record|
230
232
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
231
233
  end
232
234
 
@@ -177,14 +177,16 @@ module Bullet
177
177
 
178
178
  if Bullet.start?
179
179
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
180
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
181
180
  association = owner.association(reflection.through_reflection.name)
182
- Array(association.target).each do |through_record|
183
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
184
- end
181
+ if association.loaded?
182
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
183
+ Array.wrap(association.target).each do |through_record|
184
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
185
+ end
185
186
 
186
- if reflection.through_reflection != through_reflection
187
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
187
+ if reflection.through_reflection != through_reflection
188
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
189
+ end
188
190
  end
189
191
  end
190
192
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name) unless @inversed
@@ -226,7 +228,7 @@ module Bullet
226
228
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
227
229
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
228
230
  association = owner.association(reflection.through_reflection.name)
229
- Array(association.target).each do |through_record|
231
+ Array.wrap(association.target).each do |through_record|
230
232
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
231
233
  end
232
234
 
@@ -170,6 +170,13 @@ module Bullet
170
170
  end
171
171
  super
172
172
  end
173
+
174
+ def inversed_from_queries(record)
175
+ if Bullet.start? && inversable?(record)
176
+ Bullet::Detector::NPlusOneQuery.add_inversed_object(owner, reflection.name)
177
+ end
178
+ super
179
+ end
173
180
  end
174
181
  )
175
182
 
@@ -180,14 +187,16 @@ module Bullet
180
187
 
181
188
  if Bullet.start?
182
189
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
183
- Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
184
190
  association = owner.association(reflection.through_reflection.name)
185
- Array(association.target).each do |through_record|
186
- Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
187
- end
191
+ if association.loaded?
192
+ Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
193
+ Array.wrap(association.target).each do |through_record|
194
+ Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
195
+ end
188
196
 
189
- if reflection.through_reflection != through_reflection
190
- Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
197
+ if reflection.through_reflection != through_reflection
198
+ Bullet::Detector::NPlusOneQuery.call_association(owner, through_reflection.name)
199
+ end
191
200
  end
192
201
  end
193
202
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.name)
@@ -229,7 +238,7 @@ module Bullet
229
238
  if is_a? ::ActiveRecord::Associations::ThroughAssociation
230
239
  Bullet::Detector::NPlusOneQuery.call_association(owner, reflection.through_reflection.name)
231
240
  association = owner.association(reflection.through_reflection.name)
232
- Array(association.target).each do |through_record|
241
+ Array.wrap(association.target).each do |through_record|
233
242
  Bullet::Detector::NPlusOneQuery.call_association(through_record, source_reflection.name)
234
243
  end
235
244
 
@@ -13,6 +13,7 @@ module Bullet
13
13
  'Detector::Association#add_object_associations',
14
14
  "object: #{object.bullet_key}, associations: #{associations}"
15
15
  )
16
+ call_stacks.add(object.bullet_key)
16
17
  object_associations.add(object.bullet_key, associations)
17
18
  end
18
19
 
@@ -25,6 +26,7 @@ module Bullet
25
26
  'Detector::Association#add_call_object_associations',
26
27
  "object: #{object.bullet_key}, associations: #{associations}"
27
28
  )
29
+ call_stacks.add(object.bullet_key)
28
30
  call_object_associations.add(object.bullet_key, associations)
29
31
  end
30
32
 
@@ -76,6 +78,12 @@ module Bullet
76
78
  def eager_loadings
77
79
  Thread.current[:bullet_eager_loadings]
78
80
  end
81
+
82
+ # cal_stacks keeps stacktraces where querie-objects were called from.
83
+ # e.g. { 'Object:111' => [SomeProject/app/controllers/...] }
84
+ def call_stacks
85
+ Thread.current[:bullet_call_stacks]
86
+ end
79
87
  end
80
88
  end
81
89
  end
@@ -20,7 +20,7 @@ module Bullet
20
20
  return unless Bullet.start?
21
21
  return unless Bullet.counter_cache_enable?
22
22
 
23
- objects = Array(object_or_objects)
23
+ objects = Array.wrap(object_or_objects)
24
24
  return if objects.map(&:bullet_primary_key_value).compact.empty?
25
25
 
26
26
  Bullet.debug(
@@ -54,7 +54,7 @@ module Bullet
54
54
  private
55
55
 
56
56
  def create_notification(klazz, associations)
57
- notify_associations = Array(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
57
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:counter_cache, klazz)
58
58
 
59
59
  if notify_associations.present?
60
60
  notice = Bullet::Notification::CounterCache.new klazz, notify_associations
@@ -7,7 +7,7 @@ module Bullet
7
7
  extend StackTraceFilter
8
8
 
9
9
  class << self
10
- # executed when object.assocations is called.
10
+ # executed when object.associations is called.
11
11
  # first, it keeps this method call for object.association.
12
12
  # then, it checks if this associations call is unpreload.
13
13
  # if it is, keeps this unpreload associations and caller.
@@ -25,7 +25,7 @@ module Bullet
25
25
  )
26
26
  if !excluded_stacktrace_path? && conditions_met?(object, associations)
27
27
  Bullet.debug('detect n + 1 query', "object: #{object.bullet_key}, associations: #{associations}")
28
- create_notification caller_in_project, object.class.to_s, associations
28
+ create_notification caller_in_project(object.bullet_key), object.class.to_s, associations
29
29
  end
30
30
  end
31
31
 
@@ -33,15 +33,26 @@ module Bullet
33
33
  return unless Bullet.start?
34
34
  return unless Bullet.n_plus_one_query_enable?
35
35
 
36
- objects = Array(object_or_objects)
37
- return if objects.map(&:bullet_primary_key_value).compact.empty?
38
- return if objects.all? { |obj| obj.class.name =~ /^HABTM_/ }
39
-
40
- Bullet.debug(
41
- 'Detector::NPlusOneQuery#add_possible_objects',
42
- "objects: #{objects.map(&:bullet_key).join(', ')}"
43
- )
44
- objects.each { |object| possible_objects.add object.bullet_key }
36
+ objects = Array.wrap(object_or_objects)
37
+ class_names_match_regex = true
38
+ primary_key_values_are_empty = true
39
+ keys_joined = ""
40
+ objects.each do |obj|
41
+ unless obj.class.name =~ /^HABTM_/
42
+ class_names_match_regex = false
43
+ end
44
+ unless obj.bullet_primary_key_value.nil?
45
+ primary_key_values_are_empty = false
46
+ end
47
+ keys_joined += "#{(keys_joined.empty?? '' : ', ')}#{obj.bullet_key}"
48
+ end
49
+ unless class_names_match_regex || primary_key_values_are_empty
50
+ Bullet.debug(
51
+ 'Detector::NPlusOneQuery#add_possible_objects',
52
+ "objects: #{keys_joined}"
53
+ )
54
+ objects.each { |object| possible_objects.add object.bullet_key }
55
+ end
45
56
  end
46
57
 
47
58
  def add_impossible_object(object)
@@ -95,7 +106,7 @@ module Bullet
95
106
  private
96
107
 
97
108
  def create_notification(callers, klazz, associations)
98
- notify_associations = Array(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
109
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:n_plus_one_query, klazz)
99
110
 
100
111
  if notify_associations.present?
101
112
  notice = Bullet::Notification::NPlusOneQuery.new(callers, klazz, notify_associations)
@@ -10,7 +10,7 @@ module Bullet
10
10
  # check if there are unused preload associations.
11
11
  # get related_objects from eager_loadings associated with object and associations
12
12
  # get call_object_association from associations of call_object_associations whose object is in related_objects
13
- # if association not in call_object_association, then the object => association - call_object_association is ununsed preload assocations
13
+ # if association not in call_object_association, then the object => association - call_object_association is ununsed preload associations
14
14
  def check_unused_preload_associations
15
15
  return unless Bullet.start?
16
16
  return unless Bullet.unused_eager_loading_enable?
@@ -20,7 +20,7 @@ module Bullet
20
20
  next if object_association_diff.empty?
21
21
 
22
22
  Bullet.debug('detect unused preload', "object: #{bullet_key}, associations: #{object_association_diff}")
23
- create_notification(caller_in_project, bullet_key.bullet_class_name, object_association_diff)
23
+ create_notification(caller_in_project(bullet_key), bullet_key.bullet_class_name, object_association_diff)
24
24
  end
25
25
  end
26
26
 
@@ -65,7 +65,7 @@ module Bullet
65
65
  private
66
66
 
67
67
  def create_notification(callers, klazz, associations)
68
- notify_associations = Array(associations) - Bullet.get_safelist_associations(:unused_eager_loading, klazz)
68
+ notify_associations = Array.wrap(associations) - Bullet.get_safelist_associations(:unused_eager_loading, klazz)
69
69
 
70
70
  if notify_associations.present?
71
71
  notice = Bullet::Notification::UnusedEagerLoading.new(callers, klazz, notify_associations)
@@ -4,22 +4,20 @@ module Bullet
4
4
  module Mongoid
5
5
  def self.enable
6
6
  require 'mongoid'
7
+ require 'rubygems'
7
8
  ::Mongoid::Contextual::Mongo.class_eval do
8
9
  alias_method :origin_first, :first
9
10
  alias_method :origin_last, :last
10
11
  alias_method :origin_each, :each
11
12
  alias_method :origin_eager_load, :eager_load
12
13
 
13
- def first(opts = {})
14
- result = origin_first(opts)
15
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
16
- result
17
- end
18
-
19
- def last(opts = {})
20
- result = origin_last(opts)
21
- Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
22
- result
14
+ %i[first last].each do |context|
15
+ default = Gem::Version.new(::Mongoid::VERSION) >= Gem::Version.new('7.5') ? nil : {}
16
+ define_method(context) do |opts = default|
17
+ result = send(:"origin_#{context}", opts)
18
+ Bullet::Detector::NPlusOneQuery.add_impossible_object(result) if result
19
+ result
20
+ end
23
21
  end
24
22
 
25
23
  def each(&block)
data/lib/bullet/rack.rb CHANGED
@@ -4,6 +4,8 @@ module Bullet
4
4
  class Rack
5
5
  include Dependency
6
6
 
7
+ NONCE_MATCHER = /script-src .*'nonce-(?<nonce>[A-Za-z0-9+\/]+={0,2})'/
8
+
7
9
  def initialize(app)
8
10
  @app = app
9
11
  end
@@ -20,11 +22,15 @@ module Bullet
20
22
  if Bullet.inject_into_page? && !file?(headers) && !sse?(headers) && !empty?(response) && status == 200
21
23
  if html_request?(headers, response)
22
24
  response_body = response_body(response)
23
- response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
24
- response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
25
- if Bullet.add_footer && !Bullet.skip_http_headers
26
- response_body = append_to_html_body(response_body, xhr_script)
25
+
26
+ with_security_policy_nonce(headers) do |nonce|
27
+ response_body = append_to_html_body(response_body, footer_note) if Bullet.add_footer
28
+ response_body = append_to_html_body(response_body, Bullet.gather_inline_notifications)
29
+ if Bullet.add_footer && !Bullet.skip_http_headers
30
+ response_body = append_to_html_body(response_body, xhr_script(nonce))
31
+ end
27
32
  end
33
+
28
34
  headers['Content-Length'] = response_body.bytesize.to_s
29
35
  elsif !Bullet.skip_http_headers
30
36
  set_header(headers, 'X-bullet-footer-text', Bullet.footer_info.uniq) if Bullet.add_footer
@@ -118,8 +124,34 @@ module Bullet
118
124
  end
119
125
 
120
126
  # Make footer work for XHR requests by appending data to the footer
121
- def xhr_script
122
- "<script type='text/javascript'>#{File.read("#{__dir__}/bullet_xhr.js")}</script>"
127
+ def xhr_script(nonce = nil)
128
+ script = File.read("#{__dir__}/bullet_xhr.js")
129
+
130
+ if nonce
131
+ "<script type='text/javascript' nonce='#{nonce}'>#{script}</script>"
132
+ else
133
+ "<script type='text/javascript'>#{script}</script>"
134
+ end
135
+ end
136
+
137
+ def with_security_policy_nonce(headers)
138
+ matched = (headers['Content-Security-Policy'] || '').match(NONCE_MATCHER)
139
+ nonce = matched[:nonce] if matched
140
+
141
+ if nonce
142
+ console_enabled = UniformNotifier.console
143
+ alert_enabled = UniformNotifier.alert
144
+
145
+ UniformNotifier.console = { attributes: { nonce: nonce } } if console_enabled
146
+ UniformNotifier.alert = { attributes: { nonce: nonce } } if alert_enabled
147
+
148
+ yield nonce
149
+
150
+ UniformNotifier.console = console_enabled
151
+ UniformNotifier.alert = alert_enabled
152
+ else
153
+ yield
154
+ end
123
155
  end
124
156
  end
125
157
  end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bullet
4
+ module Registry
5
+ class CallStack < Base
6
+ # remembers found association backtrace
7
+ def add(key)
8
+ @registry[key] = Thread.current.backtrace
9
+ end
10
+ end
11
+ end
12
+ end
@@ -5,5 +5,6 @@ module Bullet
5
5
  autoload :Base, 'bullet/registry/base'
6
6
  autoload :Object, 'bullet/registry/object'
7
7
  autoload :Association, 'bullet/registry/association'
8
+ autoload :CallStack, 'bullet/registry/call_stack'
8
9
  end
9
10
  end
@@ -6,10 +6,11 @@ module Bullet
6
6
  VENDOR_PATH = '/vendor'
7
7
  IS_RUBY_19 = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.0.0')
8
8
 
9
- def caller_in_project
9
+ # @param bullet_key[String] - use this to get stored call stack from call_stacks object.
10
+ def caller_in_project(bullet_key = nil)
10
11
  vendor_root = Bullet.app_root + VENDOR_PATH
11
12
  bundler_path = Bundler.bundle_path.to_s
12
- select_caller_locations do |location|
13
+ select_caller_locations(bullet_key) do |location|
13
14
  caller_path = location_as_path(location)
14
15
  caller_path.include?(Bullet.app_root) && !caller_path.include?(vendor_root) &&
15
16
  !caller_path.include?(bundler_path) || Bullet.stacktrace_includes.any? { |include_pattern|
@@ -50,15 +51,16 @@ module Bullet
50
51
  end
51
52
 
52
53
  def location_as_path(location)
54
+ return location if location.is_a?(String)
55
+
53
56
  IS_RUBY_19 ? location : location.absolute_path.to_s
54
57
  end
55
58
 
56
- def select_caller_locations
57
- if IS_RUBY_19
58
- caller.select { |caller_path| yield caller_path }
59
- else
60
- caller_locations.select { |location| yield location }
61
- end
59
+ def select_caller_locations(bullet_key = nil)
60
+ return caller.select { |caller_path| yield caller_path } if IS_RUBY_19
61
+
62
+ call_stack = bullet_key ? call_stacks[bullet_key] : caller_locations
63
+ call_stack.select { |location| yield location }
62
64
  end
63
65
  end
64
66
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bullet
4
- VERSION = '7.0.2'
4
+ VERSION = '7.0.7'
5
5
  end
data/lib/bullet.rb CHANGED
@@ -23,7 +23,11 @@ module Bullet
23
23
  if defined?(Rails::Railtie)
24
24
  class BulletRailtie < Rails::Railtie
25
25
  initializer 'bullet.configure_rails_initialization' do |app|
26
- app.middleware.use Bullet::Rack
26
+ if defined?(ActionDispatch::ContentSecurityPolicy::Middleware) && Rails.application.config.content_security_policy
27
+ app.middleware.insert_before ActionDispatch::ContentSecurityPolicy::Middleware, Bullet::Rack
28
+ else
29
+ app.middleware.use Bullet::Rack
30
+ end
27
31
  end
28
32
  end
29
33
  end
@@ -109,7 +113,7 @@ module Bullet
109
113
  end
110
114
 
111
115
  def get_safelist_associations(type, class_name)
112
- Array(@safelist[type][class_name])
116
+ Array.wrap(@safelist[type][class_name])
113
117
  end
114
118
 
115
119
  def reset_safelist
@@ -144,6 +148,7 @@ module Bullet
144
148
  Thread.current[:bullet_impossible_objects] = Bullet::Registry::Object.new
145
149
  Thread.current[:bullet_inversed_objects] = Bullet::Registry::Base.new
146
150
  Thread.current[:bullet_eager_loadings] = Bullet::Registry::Association.new
151
+ Thread.current[:bullet_call_stacks] = Bullet::Registry::CallStack.new
147
152
 
148
153
  Thread.current[:bullet_counter_possible_objects] ||= Bullet::Registry::Object.new
149
154
  Thread.current[:bullet_counter_impossible_objects] ||= Bullet::Registry::Object.new
@@ -39,7 +39,7 @@ module Bullet
39
39
 
40
40
  it 'should be false if object, association pair is not existed' do
41
41
  NPlusOneQuery.add_object_associations(@post, :association1)
42
- expect(NPlusOneQuery.association?(@post, :associatio2)).to eq false
42
+ expect(NPlusOneQuery.association?(@post, :association2)).to eq false
43
43
  end
44
44
  end
45
45
 
@@ -127,38 +127,6 @@ module Bullet
127
127
  end
128
128
  end
129
129
 
130
- context '.caller_in_project' do
131
- it 'should include only paths that are in the project' do
132
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
133
- not_in_project = OpenStruct.new(absolute_path: '/def/def.rb')
134
-
135
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, not_in_project])
136
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
137
- expect(NPlusOneQuery).to receive(:create_notification).with([in_project], 'Post', :association)
138
- NPlusOneQuery.call_association(@post, :association)
139
- end
140
-
141
- context 'stacktrace_includes' do
142
- before { Bullet.stacktrace_includes = ['def', /xyz/] }
143
- after { Bullet.stacktrace_includes = nil }
144
-
145
- it 'should include paths that are in the stacktrace_include list' do
146
- in_project = OpenStruct.new(absolute_path: File.join(Dir.pwd, 'abc', 'abc.rb'))
147
- included_gems = [OpenStruct.new(absolute_path: '/def/def.rb'), OpenStruct.new(absolute_path: 'xyz/xyz.rb')]
148
- excluded_gem = OpenStruct.new(absolute_path: '/ghi/ghi.rb')
149
-
150
- expect(NPlusOneQuery).to receive(:caller_locations).and_return([in_project, *included_gems, excluded_gem])
151
- expect(NPlusOneQuery).to receive(:conditions_met?).with(@post, :association).and_return(true)
152
- expect(NPlusOneQuery).to receive(:create_notification).with(
153
- [in_project, *included_gems],
154
- 'Post',
155
- :association
156
- )
157
- NPlusOneQuery.call_association(@post, :association)
158
- end
159
- end
160
- end
161
-
162
130
  context '.add_possible_objects' do
163
131
  it 'should add possible objects' do
164
132
  NPlusOneQuery.add_possible_objects([@post, @post2])
@@ -65,6 +65,11 @@ module Bullet
65
65
  expect(UnusedEagerLoading).not_to receive(:create_notification).with('Post', [:association])
66
66
  UnusedEagerLoading.check_unused_preload_associations
67
67
  end
68
+
69
+ it 'should create call stack for notification' do
70
+ UnusedEagerLoading.add_object_associations(@post, :association)
71
+ expect(UnusedEagerLoading.send(:call_stacks).registry).not_to be_empty
72
+ end
68
73
  end
69
74
 
70
75
  context '.add_eager_loadings' do
@@ -50,7 +50,7 @@ module Bullet
50
50
  end
51
51
 
52
52
  it 'should be true if no response body' do
53
- response = double()
53
+ response = double
54
54
  expect(middleware).to be_empty(response)
55
55
  end
56
56
  end
@@ -129,6 +129,23 @@ module Bullet
129
129
  expect(response).to eq(%w[<html><head></head><body><bullet></bullet></body></html>])
130
130
  end
131
131
 
132
+ it 'should include CSP nonce in inline script if console_enabled and a CSP is applied' do
133
+ allow(Bullet).to receive(:add_footer).at_least(:once).and_return(true)
134
+ expect(Bullet).to receive(:console_enabled?).and_return(true)
135
+ allow(middleware).to receive(:xhr_script).and_call_original
136
+
137
+ nonce = '+t9/wTlgG6xbHxXYUaDNzQ=='
138
+ app.headers = {
139
+ 'Content-Type' => 'text/html',
140
+ 'Content-Security-Policy' => "default-src 'self' https:; script-src 'self' https: 'nonce-#{nonce}'"
141
+ }
142
+
143
+ _, headers, response = middleware.call('Content-Type' => 'text/html')
144
+
145
+ size = 56 + middleware.send(:footer_note).length + middleware.send(:xhr_script, nonce).length
146
+ expect(headers['Content-Length']).to eq(size.to_s)
147
+ end
148
+
132
149
  it 'should change response body for html safe string if console_enabled is true' do
133
150
  expect(Bullet).to receive(:console_enabled?).and_return(true)
134
151
  app.response =
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module Bullet
6
+ RSpec.describe StackTraceFilter do
7
+ let(:dummy_class) { Class.new { extend StackTraceFilter } }
8
+ let(:root_path) { Dir.pwd }
9
+ let(:bundler_path) { Bundler.bundle_path }
10
+
11
+ describe '#caller_in_project' do
12
+ it 'gets the caller in the project' do
13
+ expect(dummy_class).to receive(:call_stacks).and_return({
14
+ 'Post:1' => [
15
+ File.join(root_path, 'lib/bullet.rb'),
16
+ File.join(root_path, 'vendor/uniform_notifier.rb'),
17
+ File.join(bundler_path, 'rack.rb')
18
+ ]
19
+ })
20
+ expect(dummy_class.caller_in_project('Post:1')).to eq([
21
+ File.join(root_path, 'lib/bullet.rb')
22
+ ])
23
+ end
24
+ end
25
+ end
26
+ end
@@ -58,6 +58,19 @@ if active_record?
58
58
  expect(Bullet::Detector::Association).to be_completely_preloading_associations
59
59
  end
60
60
 
61
+ it 'should detect non preload comment => post with inverse_of from a query' do
62
+ Post.first.comments.find_each do |comment|
63
+ comment.name
64
+ comment.post.name
65
+ end
66
+
67
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
68
+ expect(Post.first.comments.count).not_to eq(0)
69
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
70
+
71
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
72
+ end
73
+
61
74
  it 'should detect non preload post => comments with empty?' do
62
75
  Post.all.each { |post| post.comments.empty? }
63
76
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
@@ -474,7 +487,15 @@ if active_record?
474
487
  end
475
488
 
476
489
  it 'should detect preload associations' do
477
- Firm.includes(:clients).each { |firm| firm.clients.map(&:name) }
490
+ Firm.preload(:clients).each { |firm| firm.clients.map(&:name) }
491
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
492
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
493
+
494
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
495
+ end
496
+
497
+ it 'should detect eager load association' do
498
+ Firm.eager_load(:clients).each { |firm| firm.clients.map(&:name) }
478
499
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
479
500
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
480
501
 
@@ -508,7 +529,15 @@ if active_record?
508
529
  end
509
530
 
510
531
  it 'should detect preload associations' do
511
- Firm.includes(:groups).each { |firm| firm.groups.map(&:name) }
532
+ Firm.preload(:groups).each { |firm| firm.groups.map(&:name) }
533
+ Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
534
+ expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
535
+
536
+ expect(Bullet::Detector::Association).to be_completely_preloading_associations
537
+ end
538
+
539
+ it 'should detect eager load associations' do
540
+ Firm.eager_load(:groups).each { |firm| firm.groups.map(&:name) }
512
541
  Bullet::Detector::UnusedEagerLoading.check_unused_preload_associations
513
542
  expect(Bullet::Detector::Association).not_to be_has_unused_preload_associations
514
543
 
@@ -92,7 +92,7 @@ module Support
92
92
  page3 = Page.create(name: 'page3', parent_id: folder2.id, author_id: author2.id)
93
93
  page4 = Page.create(name: 'page4', parent_id: folder2.id, author_id: author2.id)
94
94
 
95
- role1 = Role.create(name: 'Amdin')
95
+ role1 = Role.create(name: 'Admin')
96
96
  role2 = Role.create(name: 'User')
97
97
 
98
98
  user1 = User.create(name: 'user1', category: category1)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.0.2
4
+ version: 7.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Richard Huang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-05-31 00:00:00.000000000 Z
11
+ date: 2023-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -104,6 +104,7 @@ files:
104
104
  - lib/bullet/registry.rb
105
105
  - lib/bullet/registry/association.rb
106
106
  - lib/bullet/registry/base.rb
107
+ - lib/bullet/registry/call_stack.rb
107
108
  - lib/bullet/registry/object.rb
108
109
  - lib/bullet/stack_trace_filter.rb
109
110
  - lib/bullet/version.rb
@@ -126,6 +127,7 @@ files:
126
127
  - spec/bullet/registry/association_spec.rb
127
128
  - spec/bullet/registry/base_spec.rb
128
129
  - spec/bullet/registry/object_spec.rb
130
+ - spec/bullet/stack_trace_filter_spec.rb
129
131
  - spec/bullet_spec.rb
130
132
  - spec/integration/active_record/association_spec.rb
131
133
  - spec/integration/counter_cache_spec.rb
@@ -195,7 +197,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
195
197
  - !ruby/object:Gem::Version
196
198
  version: 1.3.6
197
199
  requirements: []
198
- rubygems_version: 3.3.7
200
+ rubygems_version: 3.4.1
199
201
  signing_key:
200
202
  specification_version: 4
201
203
  summary: help to kill N+1 queries and unused eager loading.
@@ -216,6 +218,7 @@ test_files:
216
218
  - spec/bullet/registry/association_spec.rb
217
219
  - spec/bullet/registry/base_spec.rb
218
220
  - spec/bullet/registry/object_spec.rb
221
+ - spec/bullet/stack_trace_filter_spec.rb
219
222
  - spec/bullet_spec.rb
220
223
  - spec/integration/active_record/association_spec.rb
221
224
  - spec/integration/counter_cache_spec.rb