activity_notification 2.7.0 → 2.7.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bafe2d6977f08838087612f2d58947783a2e6add85633d85abe024cbe3a8041b
4
- data.tar.gz: '095b581d3e475f27bfd186265083ea42f953b9b7c992c963fbb5fdd40cedd96e'
3
+ metadata.gz: 9c4c8885cdeaf0c979ae2474e5ca5459a16e0f14c605fa5d2f0ad37076e7c793
4
+ data.tar.gz: 267223c3dbc745920baabb75d5e936cafef5a3fbe2e301f46aab04ae9d6a34a8
5
5
  SHA512:
6
- metadata.gz: f6c687e0c9df39d2950bd2f2b2f2da4e10b95404315e09a9c91b8002cbaf98140a33877700c5c5b8a36b46f3387658caba039c722a91664b52d56ec29eac47d8
7
- data.tar.gz: a9ebe7817f6c78fceb53e5ce0adee2b20e7b964e97c5ad812933a2d097d42f3d1617a29d26b0b07225309d56ebb2168fdcf64bd26c4d3478523ed6d1354de0ff
6
+ metadata.gz: bcb53649395b3e43ab8e3b4a043a40947305ee35cd15fe16a9737f16c866ff543749e78b00c5c5659d8a34eaf198a6c30b6933d29a048db00d56ea17b7c4694e
7
+ data.tar.gz: cc390e51688280d2c84bb949814ae6a3ecad9f1c444066b45d91316d47fea5bb71a7dec03fd4a582d503cc5f7aad70e47c9c0f7ddf16b1da1f92ae1485c10595
@@ -4,6 +4,8 @@ class ActivityNotification::NotificationApiChannel < ActivityNotification::Notif
4
4
  # ActionCable::Channel::Base#subscribed
5
5
  # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
6
6
  def subscribed
7
+ # before_subscribe callbacks do not halt the chain on reject
8
+ return if subscription_rejected?
7
9
  stream_from "#{ActivityNotification.config.notification_api_channel_prefix}_#{@target.to_class_name}#{ActivityNotification.config.composite_key_delimiter}#{@target.id}"
8
10
  rescue
9
11
  reject
@@ -7,6 +7,8 @@ if defined?(ActionCable)
7
7
  # ActionCable::Channel::Base#subscribed
8
8
  # @see https://api.rubyonrails.org/classes/ActionCable/Channel/Base.html#method-i-subscribed
9
9
  def subscribed
10
+ # before_subscribe callbacks do not halt the chain on reject
11
+ return if subscription_rejected?
10
12
  stream_from "#{ActivityNotification.config.notification_channel_prefix}_#{@target.to_class_name}#{ActivityNotification.config.composite_key_delimiter}#{@target.id}"
11
13
  rescue
12
14
  reject
@@ -5,7 +5,6 @@ module ActivityNotification
5
5
  include Swagger::NotificationsApi
6
6
  # Include CommonApiController to select target and define common methods
7
7
  include CommonApiController
8
- protect_from_forgery except: [:open_all]
9
8
  rescue_from ActivityNotification::NotifiableNotFoundError, with: :render_notifiable_not_found
10
9
 
11
10
  # Returns notification index of the target.
@@ -4,6 +4,7 @@ module ActivityNotification
4
4
  # Include CommonController to select target and define common methods
5
5
  include CommonController
6
6
  before_action :set_notification, except: [:index, :open_all, :destroy_all]
7
+ before_action :validate_time_params, only: [:index, :open_all, :destroy_all]
7
8
 
8
9
  # Shows notification index of the target.
9
10
  #
@@ -125,6 +126,7 @@ module ActivityNotification
125
126
  # @overload open(params)
126
127
  # @param [Hash] params Request parameters
127
128
  # @option params [String] :open ('false') Whether the notification will be opened
129
+ # @option params [String] :open_token (nil) Token to open the notification, which is required with open parameter since GET requests are not protected from CSRF (See Notification#open_token)
128
130
  # @option params [String] :filter (nil) Filter option to load notification index by their status (Nothing as auto, 'opened' or 'unopened')
129
131
  # @option params [String] :limit (nil) Maximum number of notifications to return
130
132
  # @option params [String] :without_grouping ('false') Whether notification index will include group members
@@ -133,7 +135,7 @@ module ActivityNotification
133
135
  # @return [Response] JavaScript view for ajax request or redirects to back as default
134
136
  def move
135
137
  with_members = !(params[:with_group_members].to_s.to_boolean(false) || params[:without_grouping].to_s.to_boolean(false))
136
- @opened_notifications_count = @notification.open!(with_members: with_members) if params[:open].to_s.to_boolean(false)
138
+ @opened_notifications_count = @notification.open!(with_members: with_members) if params[:open].to_s.to_boolean(false) && @notification.valid_open_token?(params[:open_token])
137
139
  redirect_to_notifiable_path
138
140
  end
139
141
 
@@ -171,7 +173,7 @@ module ActivityNotification
171
173
  # @api protected
172
174
  # @return [Hash] options to load notification index
173
175
  def set_index_options
174
- limit = params[:limit].to_i > 0 ? params[:limit].to_i : nil
176
+ limit = index_limit
175
177
  reverse = params[:reverse].present? ?
176
178
  params[:reverse].to_s.to_boolean(false) : nil
177
179
  with_group_members = params[:with_group_members].present? || params[:without_grouping].present? ? params[:with_group_members].to_s.to_boolean(false) || params[:without_grouping].to_s.to_boolean(false) : nil
@@ -180,6 +182,21 @@ module ActivityNotification
180
182
  .merge(limit: limit, reverse: reverse, with_group_members: with_group_members)
181
183
  end
182
184
 
185
+ # Validates ISO 8601 time parameters and returns HTTP 400 when they are invalid.
186
+ # @api protected
187
+ # @return [void]
188
+ def validate_time_params
189
+ [:later_than, :earlier_than].each do |param_name|
190
+ next unless params.key?(param_name)
191
+ begin
192
+ Time.iso8601(params[param_name].to_s)
193
+ rescue ArgumentError
194
+ render_invalid_parameter("Parameter is not ISO 8601 time: #{param_name}")
195
+ return
196
+ end
197
+ end
198
+ end
199
+
183
200
  # Loads notification index with request parameters.
184
201
  # @api protected
185
202
  # @return [Array] Array of notification index
@@ -5,7 +5,6 @@ module ActivityNotification
5
5
  include Swagger::SubscriptionsApi
6
6
  # Include CommonApiController to select target and define common methods
7
7
  include CommonApiController
8
- protect_from_forgery except: [:create]
9
8
  before_action :set_subscription, except: [:index, :create, :find, :optional_target_names]
10
9
  before_action ->{ validate_param(:key) }, only: [:find, :optional_target_names]
11
10
 
@@ -198,7 +198,7 @@ module ActivityNotification
198
198
  # @api protected
199
199
  # @return [Hash] options to load subscription index
200
200
  def set_index_options
201
- limit = params[:limit].to_i > 0 ? params[:limit].to_i : nil
201
+ limit = index_limit
202
202
  reverse = params[:reverse].present? ? params[:reverse].to_s.to_boolean(false) : nil
203
203
  @index_options = params.permit(:filter, :filtered_by_key, :routing_scope, :devise_default_routes)
204
204
  .to_h.symbolize_keys.merge(limit: limit, reverse: reverse)
@@ -40,7 +40,7 @@
40
40
  </style>
41
41
 
42
42
  <% if @target.notification_action_cable_allowed? %>
43
- <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js"></script>
43
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js" integrity="sha512-eiqtDDb4GUVCSqOSOTz/s/eiU4B31GrdSb17aPAA4Lv/Cjc8o+hnDvuNkgXhSI5yHuDvYkuojMaQmrB5JB31XQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
44
44
  <script>
45
45
  App.activity_notification = App.cable.subscriptions.create(
46
46
  {
data/docs/Functions.md CHANGED
@@ -336,6 +336,8 @@ You can also add conditions to filter notifications, like this:
336
336
  User.send_batch_unopened_notification_email(batch_key: 'batch.comment.post', filtered_by_key: 'comment.post', custom_filter: ["created_at >= ?", time.hour.ago])
337
337
  ```
338
338
 
339
+ *custom_filter* accepts an array of a SQL fragment with bind parameters or a hash of conditions. Write it in your application code and never build it from request parameters, since it is passed to the ORM query layer as is. A raw SQL string, an array without bind parameters and MongoDB JavaScript operators (`$where`, `$function` and `$accumulator`) are rejected with `ArgumentError`.
340
+
339
341
  #### Batch sender configuration
340
342
 
341
343
  *activity_notification* uses same sender configuration of real-time email notification as batch email sender.
@@ -1007,7 +1009,7 @@ Then, *activity_notification* will broadcast configured notifications to target
1007
1009
  You can simply create subscriptions for the specified target in your view like this:
1008
1010
 
1009
1011
  ```js
1010
- <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js"></script>
1012
+ <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/push.js/1.0.9/push.min.js" integrity="sha512-eiqtDDb4GUVCSqOSOTz/s/eiU4B31GrdSb17aPAA4Lv/Cjc8o+hnDvuNkgXhSI5yHuDvYkuojMaQmrB5JB31XQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
1011
1013
  <script>
1012
1014
  App.activity_notification = App.cable.subscriptions.create(
1013
1015
  {
@@ -1045,7 +1047,7 @@ You can simply create subscriptions for the specified target in your view like t
1045
1047
  or create subscriptions in your single page application with API channels like this:
1046
1048
 
1047
1049
  ```js
1048
- // Vue.js implementation with actioncable-vue
1050
+ // Vue.js implementation with a small Action Cable plugin (see spec/rails_app/app/javascript/plugins/action-cable.js)
1049
1051
  export default {
1050
1052
  // ...
1051
1053
  mounted () {
@@ -707,6 +707,23 @@ module ActivityNotification
707
707
  unopened_member_count + 1
708
708
  end
709
709
 
710
+ # Returns a token to open the notification by HTTP GET request to move action.
711
+ # GET requests are not protected from CSRF, so move action opens the notification only with this token.
712
+ # move_notification_path_for and move_notification_url_for helpers add this token when open: true is specified.
713
+ #
714
+ # @return [String] Token to open the notification by move action
715
+ def open_token
716
+ ActivityNotification.open_token_verifier.generate(id.to_s, purpose: :open_notification)
717
+ end
718
+
719
+ # Returns if the token is valid to open the notification by move action.
720
+ #
721
+ # @param [String] token Token to verify
722
+ # @return [Boolean] If the token is valid to open the notification
723
+ def valid_open_token?(token)
724
+ token.is_a?(String) && ActivityNotification.open_token_verifier.verified(token, purpose: :open_notification) == id.to_s
725
+ end
726
+
710
727
  # Returns if the notification is unopened.
711
728
  #
712
729
  # @return [Boolean] If the notification is unopened
@@ -47,24 +47,57 @@ module ActivityNotification
47
47
  end
48
48
  end
49
49
 
50
+ # MongoDB query operators which execute JavaScript on the database server.
51
+ JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
52
+
50
53
  # Validates a :custom_filter value before it is passed to the ORM query layer.
51
- # A String is rejected because ActiveRecord's where treats a String as a raw
52
- # SQL fragment, which allows SQL injection when the value originates from
53
- # untrusted input. The documented and supported forms are an Array of a SQL
54
- # fragment with bind parameters (e.g. ["created_at >= ?", time]) or a Hash of
55
- # conditions, both of which are safe.
54
+ # custom_filter is a trusted input written by the application developer and
55
+ # must not be built from request parameters. This validation rejects the forms
56
+ # that run raw SQL or server-side JavaScript:
57
+ # - A String, since ActiveRecord's where treats it as a raw SQL fragment
58
+ # - An Array without bind parameters (e.g. ["1=1"]), which is also a raw SQL fragment
59
+ # - A Hash containing MongoDB JavaScript operators ($where, $function and $accumulator)
60
+ # The supported forms are an Array of a SQL fragment with bind parameters
61
+ # (e.g. ["created_at >= ?", time]) or a Hash of conditions.
56
62
  # @param [Array, Hash] custom_filter Custom filter value
57
63
  # @return [Array, Hash] The same value when it is safe to use
58
- # @raise [ArgumentError] When custom_filter is a raw SQL String
64
+ # @raise [ArgumentError] When custom_filter is not in the supported forms
59
65
  def self.validate_custom_filter(custom_filter)
60
- if custom_filter.is_a?(String)
66
+ valid =
67
+ case custom_filter
68
+ when Array then custom_filter.size >= 2 && custom_filter.first.is_a?(String)
69
+ when Hash then !contains_javascript_query_operator?(custom_filter)
70
+ else false
71
+ end
72
+ unless valid
61
73
  raise ArgumentError,
62
74
  ":custom_filter must be an array of a SQL fragment and bind parameters " \
63
- "(e.g. [\"created_at >= ?\", time]) or a hash of conditions, not a raw SQL string"
75
+ "(e.g. [\"created_at >= ?\", time]) or a hash of conditions without JavaScript operators"
64
76
  end
65
77
  custom_filter
66
78
  end
67
79
 
80
+ # Returns whether the value contains MongoDB query operators which execute JavaScript.
81
+ # @param [Object] value Value to check
82
+ # @return [Boolean] Whether the value contains JavaScript query operators
83
+ def self.contains_javascript_query_operator?(value)
84
+ case value
85
+ when Hash
86
+ value.any? { |k, v| JAVASCRIPT_QUERY_OPERATORS.include?(k.to_s) || contains_javascript_query_operator?(v) }
87
+ when Array
88
+ value.any? { |v| contains_javascript_query_operator?(v) }
89
+ else
90
+ false
91
+ end
92
+ end
93
+ private_class_method :contains_javascript_query_operator?
94
+
95
+ # Returns message verifier to generate and verify tokens to open notifications by move action.
96
+ # @return [ActiveSupport::MessageVerifier] Message verifier for open tokens
97
+ def self.open_token_verifier
98
+ Rails.application.message_verifier("activity_notification_open_token")
99
+ end
100
+
68
101
  # Casts to indifferent hash
69
102
  # @param [ActionController::Parameters, Hash] hash
70
103
  # @return [HashWithIndifferentAccess] Converted indifferent hash
@@ -249,6 +249,15 @@ module ActivityNotification
249
249
  # @return [Boolean] Whether the REST API restricts embedded associated records to their id and printable_* method values.
250
250
  attr_accessor :restrict_api_response_fields
251
251
 
252
+ # @overload max_index_limit
253
+ # Returns maximum number of records that notification and subscription index actions of the controllers return
254
+ # @return [Integer, nil] Maximum number of records that index actions return.
255
+ # @overload max_index_limit=(value)
256
+ # Sets maximum number of records that notification and subscription index actions of the controllers return. The limit request parameter is capped by this value, and this value is used when the limit parameter is not specified. Set nil to remove the cap.
257
+ # @param [Integer, nil] max_index_limit The new max_index_limit
258
+ # @return [Integer, nil] Maximum number of records that index actions return.
259
+ attr_accessor :max_index_limit
260
+
252
261
  # Initialize configuration for ActivityNotification.
253
262
  # These configuration can be overridden in initializer.
254
263
  # @return [Config] A new instance of Config
@@ -282,6 +291,7 @@ module ActivityNotification
282
291
  @notification_api_channel_prefix = 'activity_notification_api_channel'
283
292
  @rescue_optional_target_errors = true
284
293
  @restrict_api_response_fields = false
294
+ @max_index_limit = 1000
285
295
  end
286
296
 
287
297
  # Sets ORM name for ActivityNotification (:active_record, :mongoid or :dynamodb)
@@ -103,6 +103,15 @@ module ActivityNotification
103
103
  render status: 400, json: error_response(code: 400, message: "Invalid parameter", type: message)
104
104
  end
105
105
 
106
+ # Returns limit for index actions from request parameter, capped by ActivityNotification.config.max_index_limit.
107
+ # @api protected
108
+ # @return [Integer, nil] Limit for index actions
109
+ def index_limit
110
+ limit = params[:limit].to_i > 0 ? params[:limit].to_i : nil
111
+ max_limit = ActivityNotification.config.max_index_limit
112
+ max_limit ? [limit || max_limit, max_limit].min : limit
113
+ end
114
+
106
115
  # Validate param and return HTTP 400 unless it presents.
107
116
  # @api protected
108
117
  # @param [String, Symbol] param_name Parameter name to validate
@@ -294,11 +294,18 @@ module ActivityNotification
294
294
  parameter do
295
295
  key :name, :open
296
296
  key :in, :query
297
- key :description, "Whether the notification will be opened"
297
+ key :description, "Whether the notification will be opened. The notification is opened only with a valid open_token, since GET requests are not protected from CSRF. Use PUT /{target_type}/{target_id}/notifications/{id}/open with move parameter to open and move without a token."
298
298
  key :required, false
299
299
  key :type, :boolean
300
300
  key :default, false
301
301
  end
302
+ parameter do
303
+ key :name, :open_token
304
+ key :in, :query
305
+ key :description, "Token to open the notification with open parameter, which is generated by Notification#open_token and added by move_notification_path_for and move_notification_url_for helpers"
306
+ key :required, false
307
+ key :type, :string
308
+ end
302
309
 
303
310
  response 302 do
304
311
  key :description, "Redirection to notifiable path"
@@ -21,7 +21,7 @@ module ActivityNotification
21
21
  notifications.render self, options
22
22
  elsif notifications.respond_to?(:map)
23
23
  return nil if (notifications.respond_to?(:empty?) ? notifications.empty? : notifications.to_a.empty?)
24
- notifications.map {|notification| notification.render self, options.dup }.join.html_safe
24
+ safe_join(notifications.map {|notification| notification.render self, options.dup })
25
25
  end
26
26
  end
27
27
  alias_method :render_notifications, :render_notification
@@ -111,7 +111,7 @@ module ActivityNotification
111
111
  # @todo Needs any other better implementation
112
112
  # @todo Must handle devise namespace
113
113
  def move_notification_path_for(notification, params = {})
114
- options = params.dup
114
+ options = move_notification_options(notification, params)
115
115
  options.delete(:devise_default_routes) ?
116
116
  send("move_#{routing_scope(options)}notification_path", notification, options) :
117
117
  send("move_#{routing_scope(options)}#{notification.target.to_resource_name}_notification_path", notification.target, notification, options)
@@ -195,7 +195,7 @@ module ActivityNotification
195
195
  # @todo Needs any other better implementation
196
196
  # @todo Must handle devise namespace
197
197
  def move_notification_url_for(notification, params = {})
198
- options = params.dup
198
+ options = move_notification_options(notification, params)
199
199
  options.delete(:devise_default_routes) ?
200
200
  send("move_#{routing_scope(options)}notification_url", notification, options) :
201
201
  send("move_#{routing_scope(options)}#{notification.target.to_resource_name}_notification_url", notification.target, notification, options)
@@ -553,6 +553,14 @@ module ActivityNotification
553
553
  [root, path].map(&:to_s).join('/')
554
554
  end
555
555
 
556
+ # Prepare options for move_notification path with token to open the notification
557
+ # @api private
558
+ def move_notification_options(notification, params)
559
+ options = params.dup
560
+ options[:open_token] = notification.open_token if options[:open].to_s.to_boolean(false)
561
+ options
562
+ end
563
+
556
564
  # Prepare routing scope from options
557
565
  # @api private
558
566
  def routing_scope(options = {})
@@ -1,3 +1,3 @@
1
1
  module ActivityNotification
2
- VERSION = "2.7.0"
2
+ VERSION = "2.7.2"
3
3
  end
@@ -129,4 +129,9 @@ ActivityNotification.configure do |config|
129
129
  # your application (see the :with_devise routing option). Default value is false.
130
130
  # config.restrict_api_response_fields = false
131
131
 
132
+ # Configure maximum number of records that notification and subscription index actions of the controllers return.
133
+ # The limit request parameter is capped by this value, and this value is used when the limit parameter is not specified.
134
+ # Set nil to remove the cap. Default value is 1000.
135
+ # config.max_index_limit = 1000
136
+
132
137
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activity_notification
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.0
4
+ version: 2.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shota Yamazaki