From 35b142a7ad19821483f900e81e915a7925fd4eaf Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 18:19:47 +0100 Subject: [PATCH 1/9] 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 c55fa0f74c7..fa4e5919211 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 f3a93987b6c3af92aee11fdb4424b8791a67e448 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/9] 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 a5c9caa4ab2..7784bec6296 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 d8f96028c54bb47e6edddbd936bc8f2301dc9fa3 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 19:53:58 +0100 Subject: [PATCH 3/9] 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 f138376b2f7..09ce1761ce6 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 8af1683e765..fc4f1598502 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 a392d60c3c0..abf53cbab6f 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 bfbd32108e4..b09472270c7 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 783b7a4f63c..e69b3596f81 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 f05b0463dbc83a85dee7490e07bd79859132ee48 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 19:58:00 +0100 Subject: [PATCH 4/9] 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 76bf9b2e55b..8a12a2b08b7 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 ed73376f1ca7ef5e254a3ec21e1ead85b2d34fd6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 1 Dec 2019 07:06:20 +0100 Subject: [PATCH 5/9] 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 2cbaa0791be..235cb7ad881 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 98ffe1c2073..13f9dfae767 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 c8d82ef3c3cb6ef3be34787c28d1c6bf8edae441 Mon Sep 17 00:00:00 2001 From: Sasha Sorokin Date: Sun, 1 Dec 2019 13:08:40 +0700 Subject: [PATCH 6/9] 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 e6fff0ad68e..0da1596ce7b 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 e69b3596f81..d498f6ce39b 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 d70268f0991ba69568112d4da5768e821d5983dd Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Sun, 1 Dec 2019 07:21:28 +0100 Subject: [PATCH 7/9] :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 cfbe487fba7..fc6c3c42f56 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 f9a8bb7c1b2..6b078c7b288 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 11799394784..2d2e1edbb57 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 fd9a5a8b9b9..fa9fd8cc447 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 From 2f8c4c588b7d212670d6b37fe13e55f717c4109b Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 30 Nov 2019 18:19:47 +0100 Subject: [PATCH 8/9] [Glitch] Fix lost focus when modals open/close Port 35b142a7ad19821483f900e81e915a7925fd4eaf to glitch-soc Signed-off-by: Thibaut Girka --- .../flavours/glitch/components/modal_root.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/components/modal_root.js b/app/javascript/flavours/glitch/components/modal_root.js index e73ef8d125c..f9877d5eadf 100644 --- a/app/javascript/flavours/glitch/components/modal_root.js +++ b/app/javascript/flavours/glitch/components/modal_root.js @@ -62,15 +62,22 @@ export default class ModalRoot extends React.PureComponent { } else if (!nextProps.children) { this.setState({ revealed: false }); } - if (!nextProps.children && !!this.props.children) { - this.activeElement.focus({ preventScroll: true }); - 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({ preventScroll: true }); + this.activeElement = null; + }).catch((error) => { + console.error(error); + }); + this.handleModalClose(); } if (this.props.children) { From fae7e0cacec60094dc1ce78d9f63b19b1cacde2d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 1 Dec 2019 07:06:20 +0100 Subject: [PATCH 9/9] [Glitch] Fix conversations not having an unread indicator in web UI Fix ed73376f1ca7ef5e254a3ec21e1ead85b2d34fd6 to glitch-soc Signed-off-by: Thibaut Girka --- .../components/conversation.js | 5 ++-- .../glitch/styles/components/index.scss | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js index a80fa824bb6..ba01f8d5c8f 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/components/conversation.js @@ -12,6 +12,7 @@ import IconButton from 'flavours/glitch/components/icon_button'; import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; import { HotKeys } from 'react-hotkeys'; import { autoPlayGif } from 'flavours/glitch/util/initial_state'; +import classNames from 'classnames'; const messages = defineMessages({ more: { id: 'status.more', defaultMessage: 'More' }, @@ -193,7 +194,7 @@ class Conversation extends ImmutablePureComponent { return ( -
+
@@ -201,7 +202,7 @@ class Conversation extends ImmutablePureComponent {
- + {unread && }
diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 5ac2403d18f..febc9551322 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -1507,6 +1507,16 @@ 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 { @@ -1554,6 +1564,22 @@ margin: 0; } } + + &--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; + } + } } .ui .flash-message {