about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/controllers/about_controller.rb6
-rw-r--r--app/controllers/auth/registrations_controller.rb11
-rw-r--r--app/controllers/concerns/registration_spam_concern.rb9
-rw-r--r--app/javascript/flavours/glitch/features/notifications/index.js8
-rw-r--r--app/javascript/flavours/glitch/packs/public.js11
-rw-r--r--app/javascript/flavours/glitch/styles/forms.scss8
-rw-r--r--app/javascript/mastodon/features/notifications/index.js8
-rw-r--r--app/javascript/packs/public.js11
-rw-r--r--app/javascript/styles/mastodon/forms.scss8
-rw-r--r--app/models/user.rb7
-rw-r--r--app/validators/registration_form_time_validator.rb9
-rw-r--r--app/views/about/_registration.html.haml3
-rw-r--r--app/views/auth/registrations/new.html.haml3
-rw-r--r--app/views/shared/_error_messages.html.haml3
14 files changed, 98 insertions, 7 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb
index 5d5db937c..5ff6990d7 100644
--- a/app/controllers/about_controller.rb
+++ b/app/controllers/about_controller.rb
@@ -1,13 +1,17 @@
 # frozen_string_literal: true
 
 class AboutController < ApplicationController
+  include RegistrationSpamConcern
+
   before_action :set_pack
+
   layout 'public'
 
   before_action :require_open_federation!, only: [:show, :more]
   before_action :set_body_classes, only: :show
   before_action :set_instance_presenter
-  before_action :set_expires_in, only: [:show, :more, :terms]
+  before_action :set_expires_in, only: [:more, :terms]
+  before_action :set_registration_form_time, only: :show
 
   skip_before_action :require_functional!, only: [:more, :terms]
 
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 23e5a22e1..6429bd969 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -2,6 +2,7 @@
 
 class Auth::RegistrationsController < Devise::RegistrationsController
   include Devise::Controllers::Rememberable
+  include RegistrationSpamConcern
 
   layout :determine_layout
 
@@ -14,6 +15,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   before_action :set_body_classes, only: [:new, :create, :edit, :update]
   before_action :require_not_suspended!, only: [:update]
   before_action :set_cache_headers, only: [:edit, :update]
+  before_action :set_registration_form_time, only: :new
 
   skip_before_action :require_functional!, only: [:edit, :update]
 
@@ -46,16 +48,17 @@ class Auth::RegistrationsController < Devise::RegistrationsController
   def build_resource(hash = nil)
     super(hash)
 
-    resource.locale      = I18n.locale
-    resource.invite_code = params[:invite_code] if resource.invite_code.blank?
-    resource.sign_up_ip  = request.remote_ip
+    resource.locale                 = I18n.locale
+    resource.invite_code            = params[:invite_code] if resource.invite_code.blank?
+    resource.registration_form_time = session[:registration_form_time]
+    resource.sign_up_ip             = request.remote_ip
 
     resource.build_account if resource.account.nil?
   end
 
   def configure_sign_up_params
     devise_parameter_sanitizer.permit(:sign_up) do |u|
-      u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement)
+      u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement, :website, :confirm_password)
     end
   end
 
diff --git a/app/controllers/concerns/registration_spam_concern.rb b/app/controllers/concerns/registration_spam_concern.rb
new file mode 100644
index 000000000..af434c985
--- /dev/null
+++ b/app/controllers/concerns/registration_spam_concern.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module RegistrationSpamConcern
+  extend ActiveSupport::Concern
+
+  def set_registration_form_time
+    session[:registration_form_time] = Time.now.utc
+  end
+end
diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js
index 97434b586..73d969517 100644
--- a/app/javascript/flavours/glitch/features/notifications/index.js
+++ b/app/javascript/flavours/glitch/features/notifications/index.js
@@ -37,10 +37,16 @@ const messages = defineMessages({
   markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
 });
 
+const getExcludedTypes = createSelector([
+  state => state.getIn(['settings', 'notifications', 'shows']),
+], (shows) => {
+  return ImmutableList(shows.filter(item => !item).keys());
+});
+
 const getNotifications = createSelector([
   state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
   state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
-  state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
+  getExcludedTypes,
   state => state.getIn(['notifications', 'items']),
 ], (showFilterBar, allowedType, excludedTypes, notifications) => {
   if (!showFilterBar || allowedType === 'all') {
diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js
index c74e5c9af..dccdbc8d0 100644
--- a/app/javascript/flavours/glitch/packs/public.js
+++ b/app/javascript/flavours/glitch/packs/public.js
@@ -151,6 +151,17 @@ function main() {
       target.style.display = 'block';
     }
   });
+
+  // Empty the honeypot fields in JS in case something like an extension
+  // automatically filled them.
+  delegate(document, '#registration_new_user,#new_user', 'submit', () => {
+    ['user_website', 'user_confirm_password', 'registration_user_website', 'registration_user_confirm_password'].forEach(id => {
+      const field = document.getElementById(id);
+      if (field) {
+        field.value = '';
+      }
+    });
+  });
 }
 
 loadPolyfills()
diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss
index d53567bc3..20dc3eb82 100644
--- a/app/javascript/flavours/glitch/styles/forms.scss
+++ b/app/javascript/flavours/glitch/styles/forms.scss
@@ -345,6 +345,7 @@ code {
   input[type=number],
   input[type=email],
   input[type=password],
+  input[type=url],
   textarea {
     box-sizing: border-box;
     font-size: 16px;
@@ -985,3 +986,10 @@ code {
     flex-direction: row;
   }
 }
+
+.input.user_confirm_password,
+.input.user_website {
+  &:not(.field_with_errors) {
+    display: none;
+  }
+}
diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js
index 73df7f49d..2e0afd863 100644
--- a/app/javascript/mastodon/features/notifications/index.js
+++ b/app/javascript/mastodon/features/notifications/index.js
@@ -32,10 +32,16 @@ const messages = defineMessages({
   markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
 });
 
+const getExcludedTypes = createSelector([
+  state => state.getIn(['settings', 'notifications', 'shows']),
+], (shows) => {
+  return ImmutableList(shows.filter(item => !item).keys());
+});
+
 const getNotifications = createSelector([
   state => state.getIn(['settings', 'notifications', 'quickFilter', 'show']),
   state => state.getIn(['settings', 'notifications', 'quickFilter', 'active']),
-  state => ImmutableList(state.getIn(['settings', 'notifications', 'shows']).filter(item => !item).keys()),
+  getExcludedTypes,
   state => state.getIn(['notifications', 'items']),
 ], (showFilterBar, allowedType, excludedTypes, notifications) => {
   if (!showFilterBar || allowedType === 'all') {
diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js
index 3f6700195..2166d8df0 100644
--- a/app/javascript/packs/public.js
+++ b/app/javascript/packs/public.js
@@ -155,6 +155,17 @@ function main() {
       target.style.display = 'block';
     }
   });
+
+  // Empty the honeypot fields in JS in case something like an extension
+  // automatically filled them.
+  delegate(document, '#registration_new_user,#new_user', 'submit', () => {
+    ['user_website', 'user_confirm_password', 'registration_user_website', 'registration_user_confirm_password'].forEach(id => {
+      const field = document.getElementById(id);
+      if (field) {
+        field.value = '';
+      }
+    });
+  });
 }
 
 loadPolyfills()
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index a54a5fded..92d89e6f2 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -354,6 +354,7 @@ code {
   input[type=number],
   input[type=email],
   input[type=password],
+  input[type=url],
   textarea {
     box-sizing: border-box;
     font-size: 16px;
@@ -994,3 +995,10 @@ code {
     flex-direction: row;
   }
 }
+
+.input.user_confirm_password,
+.input.user_website {
+  &:not(.field_with_errors) {
+    display: none;
+  }
+}
diff --git a/app/models/user.rb b/app/models/user.rb
index 9bdbac76d..984f04b4e 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -89,6 +89,13 @@ class User < ApplicationRecord
   validates_with EmailMxValidator, if: :validate_email_dns?
   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
 
+  # Those are honeypot/antispam fields
+  attr_accessor :registration_form_time, :website, :confirm_password
+
+  validates_with RegistrationFormTimeValidator, on: :create
+  validates :website, absence: true, on: :create
+  validates :confirm_password, absence: true, on: :create
+
   scope :recent, -> { order(id: :desc) }
   scope :pending, -> { where(approved: false) }
   scope :approved, -> { where(approved: true) }
diff --git a/app/validators/registration_form_time_validator.rb b/app/validators/registration_form_time_validator.rb
new file mode 100644
index 000000000..ba7c7e6c6
--- /dev/null
+++ b/app/validators/registration_form_time_validator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class RegistrationFormTimeValidator < ActiveModel::Validator
+  REGISTRATION_FORM_MIN_TIME = 3.seconds.freeze
+
+  def validate(user)
+    user.errors.add(:base, I18n.t('auth.too_fast')) if user.registration_form_time.present? && user.registration_form_time > REGISTRATION_FORM_MIN_TIME.ago
+  end
+end
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 5d159e9e6..6160ca4d4 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -10,6 +10,9 @@
       = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: closed_registrations?
       = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
 
+      = f.input :confirm_password, as: :string, placeholder: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+      = f.input :website, as: :url, placeholder: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
+
     - if approved_registrations?
       .fields-group
         = f.simple_fields_for :invite_request do |invite_request_fields|
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index cc72b87ce..de541847f 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -24,6 +24,9 @@
 
   .fields-group
     = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }
+    = f.input :confirm_password, as: :string, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: t('simple_form.labels.defaults.password')), :autocomplete => 'off' }
+
+  = f.input :website, as: :url, wrapper: :with_label, label: t('simple_form.labels.defaults.honeypot', label: 'Website'), required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.honeypot', label: 'Website'), :autocomplete => 'off' }
 
   - if approved_registrations? && !@invite.present?
     .fields-group
diff --git a/app/views/shared/_error_messages.html.haml b/app/views/shared/_error_messages.html.haml
index 28becd6c4..4916bd424 100644
--- a/app/views/shared/_error_messages.html.haml
+++ b/app/views/shared/_error_messages.html.haml
@@ -1,3 +1,6 @@
 - if object.errors.any?
   .flash-message.alert#error_explanation
     %strong= t('generic.validation_errors', count: object.errors.count)
+- object.errors[:base].each do |error|
+  .flash-message.alert
+    %strong= error