]> cat aescling's git repositories - mastodon.git/commitdiff
Fix “Email changed” notification sometimes having wrong e-mail (#13475)
authorThibG <thib@sitedethib.com>
Wed, 15 Apr 2020 14:13:44 +0000 (16:13 +0200)
committerGitHub <noreply@github.com>
Wed, 15 Apr 2020 14:13:44 +0000 (16:13 +0200)
* Fix “Email changed” notification sometimes having wrong e-mail

Fixes #6778

The root of the issue is that `send_devise_notification` was called before
the changes were properly commited to the database, causing the mailer to
pick previous values if running too early.

Devise's documentation provides guidance on how to handle that[1][2], however,
I have found it to not be working, as the following happens, in that order:
- `send_devise_notification` is called for the `email_changed` notification.
  In that case, `changed?` is false and `saved_changes?` is true, so
  if we use the former, we have the same issue.
- the `after_commit` hook is called
- `send_devise_notification` is called for the `confirmation_instructions`
  notification.
  In that case, `changed?` is still false, and `saved_changes?` still true,
  so if we use the latter, that second notification email is simply not
  going to be sent (as we would be queuing the notification *after*
  executing the after_commit hook).

This is because it may be called from either an `after_update` or
`after_commit` hook, the difference not being a call to `save` but the
transaction actually being committed to the database. This may arguably
be a bug in Devise, or Devise's notification.

The proposed workaround is inspired by Devise's documentation but checks
whether a transaction is open to make the call whether to immediately
send the notification or defer it to the `after_commit` hook.

[1]: https://www.rubydoc.info/github/plataformatec/devise/Devise%2FModels%2FAuthenticatable:send_devise_notification
[2]: https://github.com/heartcombo/devise/blob/406915cb781e38255a30ad2a0609e33952b9ec50/lib/devise/models/authenticatable.rb#L133-L194

* Fix cases when sending notifications without changing the model

* Defer sending if and only if in transaction including current record

app/models/user.rb
app/views/user_mailer/email_changed.html.haml
app/views/user_mailer/email_changed.text.erb

index 85ee5cd0643ca6092e81b3540e0c60b58fcba7aa..ae0cdf29d19c8a2fa2631671b2a7e7b1897af67a 100644 (file)
@@ -98,6 +98,7 @@ class User < ApplicationRecord
 
   before_validation :sanitize_languages
   before_create :set_approved
+  after_commit :send_pending_devise_notifications
 
   # This avoids a deprecation warning from Rails 5.1
   # It seems possible that a future release of devise-two-factor will
@@ -306,11 +307,38 @@ class User < ApplicationRecord
   protected
 
   def send_devise_notification(notification, *args)
-    devise_mailer.send(notification, self, *args).deliver_later
+    # This method can be called in `after_update` and `after_commit` hooks,
+    # but we must make sure the mailer is actually called *after* commit,
+    # otherwise it may work on stale data. To do this, figure out if we are
+    # within a transaction.
+    if ActiveRecord::Base.connection.current_transaction.try(:records)&.include?(self)
+      pending_devise_notifications << [notification, args]
+    else
+      render_and_send_devise_message(notification, *args)
+    end
   end
 
   private
 
+  def send_pending_devise_notifications
+    pending_devise_notifications.each do |notification, args|
+      render_and_send_devise_message(notification, *args)
+    end
+
+    # Empty the pending notifications array because the
+    # after_commit hook can be called multiple times which
+    # could cause multiple emails to be sent.
+    pending_devise_notifications.clear
+  end
+
+  def pending_devise_notifications
+    @pending_devise_notifications ||= []
+  end
+
+  def render_and_send_devise_message(notification, *args)
+    devise_mailer.send(notification, self, *args).deliver_later
+  end
+
   def set_approved
     self.approved = open_registrations? || valid_invitation? || external?
   end
index 7e91e87ad102ec776788774783ab94caacb14de9..96be8624d9d7ef85544a3e5f0d75524badc5f8d1 100644 (file)
@@ -38,7 +38,7 @@
                           %table.input{ align: 'center', cellspacing: 0, cellpadding: 0 }
                             %tbody
                               %tr
-                                %td= @resource.try(:unconfirmed_email) ? @resource.unconfirmed_email : @resource.email
+                                %td= @resource.unconfirmed_email
 
 %table.email-table{ cellspacing: 0, cellpadding: 0 }
   %tbody
index 345b16a2ce3df05ccf5c1fe41d2d950632a63b32..2b58415f562976f37b359e77d2fdd0ad5e5704c2 100644 (file)
@@ -4,6 +4,6 @@
 
 <%= t 'devise.mailer.email_changed.explanation' %>
 
-<%= @resource.try(:unconfirmed_email) ? @resource.unconfirmed_email : @resource.email %>
+<%= @resource.unconfirmed_email %>
 
 <%= t 'devise.mailer.email_changed.extra' %>