From 6041cfb13d5d00450a03c7b0b50bbf239c3611cc Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 18:19:47 +0100 Subject: [PATCH 1/7] Fix lost focus when modals open/close (#12437) * Fix lost focus after modal closes Regression caused by the use of the wicg-inert polyfill * Fix regression introduced by wicg-inert * Catch errors to please CodeClimate --- app/javascript/mastodon/components/modal_root.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js index c55fa0f74c..fa4e591921 100644 --- a/app/javascript/mastodon/components/modal_root.js +++ b/app/javascript/mastodon/components/modal_root.js @@ -56,15 +56,21 @@ export default class ModalRoot extends React.PureComponent { } else if (!nextProps.children) { this.setState({ revealed: false }); } - if (!nextProps.children && !!this.props.children) { - this.activeElement.focus(); - this.activeElement = null; - } } componentDidUpdate (prevProps) { if (!this.props.children && !!prevProps.children) { this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + + // Because of the wicg-inert polyfill, the activeElement may not be + // immediately selectable, we have to wait for observers to run, as + // described in https://github.com/WICG/inert#performance-and-gotchas + Promise.resolve().then(() => { + this.activeElement.focus(); + this.activeElement = null; + }).catch((error) => { + console.error(error); + }); } if (this.props.children) { requestAnimationFrame(() => { From 58e52929be80cdd0d2910595097fa3e89113b0c5 Mon Sep 17 00:00:00 2001 From: ntl-purism <57806346+ntl-purism@users.noreply.github.com> Date: Sat, 30 Nov 2019 12:44:59 -0600 Subject: [PATCH 2/7] LDAP & PAM added to OAuth password grant strategy (#7999) (#12390) When authenticating via OAuth, the resource owner password grant strategy is allowed by Mastodon, but (without this PR), it does not attempt to authenticate against LDAP or PAM. As a result, LDAP or PAM authenticated users cannot sign in to Mastodon with their email/password credentials via OAuth (for instance, for native/mobile app users). This PR fleshes out the authentication strategy supplied to doorkeeper in its initializer by looking up the user with LDAP and/or PAM when devise is configured to use LDAP/PAM backends. It attempts to follow the same logic as the Auth::SessionsController for handling email/password credentials. Note #1: Since this pull request affects an initializer, it's unclear how to add test automation. Note #2: The PAM authentication path has not been manually tested. It was added for completeness sake, and it is hoped that it can be manually tested before merging. --- config/initializers/doorkeeper.rb | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index a5c9caa4ab..7784bec629 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -8,8 +8,20 @@ Doorkeeper.configure do end resource_owner_from_credentials do |_routes| - user = User.find_by(email: request.params[:username]) - user if !user&.otp_required_for_login? && user&.valid_password?(request.params[:password]) + if Devise.ldap_authentication + user = User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] }) + end + + if Devise.pam_authentication + user ||= User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] }) + end + + if user.nil? + user = User.find_by(email: request.params[:username]) + user = nil unless user.valid_password?(request.params[:password]) + end + + user if !user&.otp_required_for_login? end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. From 4b0a6d79ddf1be02ac5d76ae37bcc12ac3be46a6 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 19:53:58 +0100 Subject: [PATCH 3/7] Add ability to filter reports by target account domain (#12154) * Add ability to filter reports by target account domain * Reword by_target_domain label --- app/controllers/admin/reports_controller.rb | 3 ++- app/helpers/admin/filter_helper.rb | 2 +- app/models/report_filter.rb | 2 ++ app/views/admin/reports/index.html.haml | 14 ++++++++++++++ config/locales/en.yml | 1 + 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb index f138376b2f..09ce1761ce 100644 --- a/app/controllers/admin/reports_controller.rb +++ b/app/controllers/admin/reports_controller.rb @@ -55,7 +55,8 @@ module Admin params.permit( :account_id, :resolved, - :target_account_id + :target_account_id, + :by_target_domain ) end diff --git a/app/helpers/admin/filter_helper.rb b/app/helpers/admin/filter_helper.rb index 8af1683e76..fc4f159850 100644 --- a/app/helpers/admin/filter_helper.rb +++ b/app/helpers/admin/filter_helper.rb @@ -2,7 +2,7 @@ module Admin::FilterHelper ACCOUNT_FILTERS = %i(local remote by_domain active pending silenced suspended username display_name email ip staff).freeze - REPORT_FILTERS = %i(resolved account_id target_account_id).freeze + REPORT_FILTERS = %i(resolved account_id target_account_id by_target_domain).freeze INVITE_FILTER = %i(available expired).freeze CUSTOM_EMOJI_FILTERS = %i(local remote by_domain shortcode).freeze TAGS_FILTERS = %i(directory reviewed unreviewed pending_review popular active name).freeze diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index a392d60c3c..abf53cbab6 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -19,6 +19,8 @@ class ReportFilter def scope_for(key, value) case key.to_sym + when :by_target_domain + Report.where(target_account: Account.where(domain: value)) when :resolved Report.resolved when :account_id diff --git a/app/views/admin/reports/index.html.haml b/app/views/admin/reports/index.html.haml index bfbd32108e..b09472270c 100644 --- a/app/views/admin/reports/index.html.haml +++ b/app/views/admin/reports/index.html.haml @@ -8,6 +8,20 @@ %li= filter_link_to t('admin.reports.unresolved'), resolved: nil %li= filter_link_to t('admin.reports.resolved'), resolved: '1' += form_tag admin_reports_url, method: 'GET', class: 'simple_form' do + .fields-group + - Admin::FilterHelper::REPORT_FILTERS.each do |key| + - if params[key].present? + = hidden_field_tag key, params[key] + + - %i(by_target_domain).each do |key| + .input.string.optional + = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.reports.#{key}") + + .actions + %button= t('admin.accounts.search') + = link_to t('admin.accounts.reset'), admin_reports_path, class: 'button negative' + - @reports.group_by(&:target_account_id).each do |target_account_id, reports| - target_account = reports.first.target_account .report-card diff --git a/config/locales/en.yml b/config/locales/en.yml index 783b7a4f63..e69b3596f8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -405,6 +405,7 @@ en: are_you_sure: Are you sure? assign_to_self: Assign to me assigned: Assigned moderator + by_target_domain: Domain of reported account comment: none: None created_at: Reported From 319875ab040e0d55e39b337fd2e39bd9b57df1e2 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 19:58:00 +0100 Subject: [PATCH 4/7] Fallback to Create audience when object has no defined audience (#12249) Fixes #11137 --- app/lib/activitypub/activity/create.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 76bf9b2e55..8a12a2b08b 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -25,6 +25,14 @@ class ActivityPub::Activity::Create < ActivityPub::Activity private + def audience_to + @object['to'] || @json['to'] + end + + def audience_cc + @object['cc'] || @json['cc'] + end + def process_status @tags = [] @mentions = [] @@ -75,7 +83,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def process_audience - (as_array(@object['to']) + as_array(@object['cc'])).uniq.each do |audience| + (as_array(audience_to) + as_array(audience_cc)).uniq.each do |audience| next if audience == ActivityPub::TagManager::COLLECTIONS[:public] # Unlike with tags, there is no point in resolving accounts we don't already @@ -291,11 +299,11 @@ class ActivityPub::Activity::Create < ActivityPub::Activity end def visibility_from_audience - if equals_or_includes?(@object['to'], ActivityPub::TagManager::COLLECTIONS[:public]) + if equals_or_includes?(audience_to, ActivityPub::TagManager::COLLECTIONS[:public]) :public - elsif equals_or_includes?(@object['cc'], ActivityPub::TagManager::COLLECTIONS[:public]) + elsif equals_or_includes?(audience_cc, ActivityPub::TagManager::COLLECTIONS[:public]) :unlisted - elsif equals_or_includes?(@object['to'], @account.followers_url) + elsif equals_or_includes?(audience_to, @account.followers_url) :private else :direct @@ -304,7 +312,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def audience_includes?(account) uri = ActivityPub::TagManager.instance.uri_for(account) - equals_or_includes?(@object['to'], uri) || equals_or_includes?(@object['cc'], uri) + equals_or_includes?(audience_to, uri) || equals_or_includes?(audience_cc, uri) end def replied_to_status @@ -415,7 +423,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def addresses_local_accounts? return true if @options[:delivered_to_account_id] - local_usernames = (as_array(@object['to']) + as_array(@object['cc'])).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } + local_usernames = (as_array(audience_to) + as_array(audience_cc)).uniq.select { |uri| ActivityPub::TagManager.instance.local_uri?(uri) }.map { |uri| ActivityPub::TagManager.instance.uri_to_local_id(uri, :username) } return false if local_usernames.empty? From d15b3468261cb1651301f940cbc49cc29a09edd0 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 1 Dec 2019 07:06:20 +0100 Subject: [PATCH 5/7] Fix conversations not having an unread indicator in web UI (#12506) --- .../components/conversation.js | 5 ++-- .../styles/mastodon/components.scss | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/direct_timeline/components/conversation.js b/app/javascript/mastodon/features/direct_timeline/components/conversation.js index 2cbaa0791b..235cb7ad88 100644 --- a/app/javascript/mastodon/features/direct_timeline/components/conversation.js +++ b/app/javascript/mastodon/features/direct_timeline/components/conversation.js @@ -12,6 +12,7 @@ import IconButton from 'mastodon/components/icon_button'; import RelativeTimestamp from 'mastodon/components/relative_timestamp'; import { HotKeys } from 'react-hotkeys'; import { autoPlayGif } from 'mastodon/initial_state'; +import classNames from 'classnames'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -158,7 +159,7 @@ class Conversation extends ImmutablePureComponent { return ( -
+
@@ -166,7 +167,7 @@ class Conversation extends ImmutablePureComponent {
- + {unread && }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 98ffe1c207..13f9dfae76 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -6517,6 +6517,16 @@ noscript { flex: 0 0 auto; padding: 10px; padding-top: 12px; + position: relative; + } + + &__unread { + display: inline-block; + background: $highlight-text-color; + border-radius: 50%; + width: 0.625rem; + height: 0.625rem; + margin: -.1ex .15em .1ex; } &__content { @@ -6564,4 +6574,20 @@ noscript { word-break: break-word; } } + + &--unread { + background: lighten($ui-base-color, 2%); + + &:focus { + background: lighten($ui-base-color, 4%); + } + + .conversation__content__info { + font-weight: 700; + } + + .conversation__content__relative-time { + color: $primary-text-color; + } + } } From 33696a4b4425eb83eadf09e231ab71f652a8ce66 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Sun, 1 Dec 2019 13:08:40 +0700 Subject: [PATCH 6/7] Split relationships page strings (#12502) Before this moment relationships managing page was using strings from other context - from counters, but in order for translators to be able to translate it relatively to the page, it must use separate strings. I've split the strings for "Following" and "Followers" and put them to "relationships" keyset in localization file. This should solve this issue. Fixes #10863 --- app/views/relationships/show.html.haml | 4 ++-- config/locales/en.yml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/relationships/show.html.haml b/app/views/relationships/show.html.haml index e6fff0ad68..0da1596ce7 100644 --- a/app/views/relationships/show.html.haml +++ b/app/views/relationships/show.html.haml @@ -8,8 +8,8 @@ .filter-subset %strong= t 'relationships.relationship' %ul - %li= filter_link_to t('accounts.following', count: current_account.following_count), relationship: nil - %li= filter_link_to t('accounts.followers', count: current_account.followers_count), relationship: 'followed_by' + %li= filter_link_to t('relationships.following'), relationship: nil + %li= filter_link_to t('relationships.followers'), relationship: 'followed_by' %li= filter_link_to t('relationships.mutual'), relationship: 'mutual' .filter-subset diff --git a/config/locales/en.yml b/config/locales/en.yml index e69b3596f8..d498f6ce39 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -925,6 +925,8 @@ en: relationships: activity: Account activity dormant: Dormant + followers: Followers + following: Following last_active: Last active most_recent: Most recent moved: Moved From 462788cb1218689a2b6ac11f9d09142652d7f2e7 Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Sun, 1 Dec 2019 07:21:28 +0100 Subject: [PATCH 7/7] :sparkles: Convert LDAP username (#12461) * :sparkles: Convert LDAP username #12021 Signed-off-by: mathieu.brunot * :bug: Fix conversion var use Signed-off-by: mathieu.brunot * :bug: Fix LDAP uid conversion test Signed-off-by: mathieu.brunot * :ok_hand: Remove comments with ref to PR Signed-off-by: mathieu.brunot * :ok_hand: Remove unnecessary paranthesis Signed-off-by: mathieu.brunot * :wrench: Move space in conversion string Signed-off-by: mathieu.brunot --- .env.nanobox | 3 +++ .env.production.sample | 3 +++ app/models/concerns/ldap_authenticable.rb | 12 ++++++++++-- config/initializers/devise.rb | 9 +++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.env.nanobox b/.env.nanobox index cfbe487fba..fc6c3c42f5 100644 --- a/.env.nanobox +++ b/.env.nanobox @@ -183,6 +183,9 @@ SMTP_FROM_ADDRESS=notifications@${APP_NAME}.nanoapp.io # LDAP_BIND_DN= # LDAP_PASSWORD= # LDAP_UID=cn +# LDAP_UID_CONVERSION_ENABLED=true +# LDAP_UID_CONVERSION_SEARCH=., - +# LDAP_UID_CONVERSION_REPLACE=_ # PAM authentication (optional) # PAM authentication uses for the email generation the "email" pam variable diff --git a/.env.production.sample b/.env.production.sample index f9a8bb7c1b..6b078c7b28 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -179,6 +179,9 @@ STREAMING_CLUSTER_NUM=1 # LDAP_PASSWORD= # LDAP_UID=cn # LDAP_SEARCH_FILTER=%{uid}=%{email} +# LDAP_UID_CONVERSION_ENABLED=true +# LDAP_UID_CONVERSION_SEARCH=., - +# LDAP_UID_CONVERSION_REPLACE=_ # PAM authentication (optional) # PAM authentication uses for the email generation the "email" pam variable diff --git a/app/models/concerns/ldap_authenticable.rb b/app/models/concerns/ldap_authenticable.rb index 1179939478..2d2e1edbb5 100644 --- a/app/models/concerns/ldap_authenticable.rb +++ b/app/models/concerns/ldap_authenticable.rb @@ -14,10 +14,18 @@ module LdapAuthenticable end def ldap_get_user(attributes = {}) - resource = joins(:account).find_by(accounts: { username: attributes[Devise.ldap_uid.to_sym].first }) + safe_username = attributes[Devise.ldap_uid.to_sym].first + if Devise.ldap_uid_conversion_enabled + keys = Regexp.union(Devise.ldap_uid_conversion_search.chars) + replacement = Devise.ldap_uid_conversion_replace + + safe_username = safe_username.gsub(keys, replacement) + end + + resource = joins(:account).find_by(accounts: { username: safe_username }) if resource.blank? - resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: attributes[Devise.ldap_uid.to_sym].first }, admin: false, external: true, confirmed_at: Time.now.utc) + resource = new(email: attributes[:mail].first, agreement: true, account_attributes: { username: safe_username }, admin: false, external: true, confirmed_at: Time.now.utc) resource.save! end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index fd9a5a8b9b..fa9fd8cc44 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -61,6 +61,12 @@ module Devise @@ldap_tls_no_verify = false mattr_accessor :ldap_search_filter @@ldap_search_filter = nil + mattr_accessor :ldap_uid_conversion_enabled + @@ldap_uid_conversion_enabled = false + mattr_accessor :ldap_uid_conversion_search + @@ldap_uid_conversion_search = nil + mattr_accessor :ldap_uid_conversion_replace + @@ldap_uid_conversion_replace = nil class Strategies::PamAuthenticatable def valid? @@ -365,5 +371,8 @@ Devise.setup do |config| config.ldap_uid = ENV.fetch('LDAP_UID', 'cn') config.ldap_tls_no_verify = ENV['LDAP_TLS_NO_VERIFY'] == 'true' config.ldap_search_filter = ENV.fetch('LDAP_SEARCH_FILTER', '%{uid}=%{email}') + config.ldap_uid_conversion_enabled = ENV['LDAP_UID_CONVERSION_ENABLED'] == 'true' + config.ldap_uid_conversion_search = ENV.fetch('LDAP_UID_CONVERSION_SEARCH', '.,- ') + config.ldap_uid_conversion_replace = ENV.fetch('LDAP_UID_CONVERSION_REPLACE', '_') end end