From f890fdca419e5f0222d49682e42d698e6353ab5d Mon Sep 17 00:00:00 2001 From: Effy Elden Date: Wed, 16 Nov 2022 21:59:28 +1100 Subject: [PATCH 01/20] Bump Helm app version to 4.0.2 (#20697) * Bump Helm app version to 4.0.1 * Bump Helm app version to 4.0.1 --- chart/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 7080095f2..7d5bd3c91 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -20,7 +20,7 @@ version: 2.3.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. -appVersion: v3.5.3 +appVersion: v4.0.2 dependencies: - name: elasticsearch From aaca78da78909dd5a23df3e70de07b838eaf4a0e Mon Sep 17 00:00:00 2001 From: nyura123dev <58617294+nyura123dev@users.noreply.github.com> Date: Thu, 17 Nov 2022 01:54:43 -0600 Subject: [PATCH 02/20] Fix safari explore disappearing tabs (#20917) * fix disappearing Explore tabs on Safari * fix lint Co-authored-by: nyura --- .../mastodon/features/explore/index.js | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/features/explore/index.js b/app/javascript/mastodon/features/explore/index.js index 552def142..286170c9f 100644 --- a/app/javascript/mastodon/features/explore/index.js +++ b/app/javascript/mastodon/features/explore/index.js @@ -24,6 +24,16 @@ const mapStateToProps = state => ({ isSearching: state.getIn(['search', 'submitted']) || !showTrends, }); +// Fix strange bug on Safari where (rendered by FormattedMessage) disappears +// after clicking around Explore top bar (issue #20885). +// Removing width=100% from also fixes it, as well as replacing with
+// We're choosing to wrap span with div to keep the changes local only to this tool bar. +const WrapFormattedMessage = ({ children, ...props }) =>
{children}
; +WrapFormattedMessage.propTypes = { + children: PropTypes.any, +}; + + export default @connect(mapStateToProps) @injectIntl class Explore extends React.PureComponent { @@ -47,7 +57,7 @@ class Explore extends React.PureComponent { this.column = c; } - render () { + render() { const { intl, multiColumn, isSearching } = this.props; const { signedIn } = this.context.identity; @@ -70,10 +80,10 @@ class Explore extends React.PureComponent { ) : (
- - - - {signedIn && } + + + + {signedIn && }
From a2931d19ae93ff4f465ac9e328abd63748daa905 Mon Sep 17 00:00:00 2001 From: trwnh Date: Thu, 17 Nov 2022 03:50:21 -0600 Subject: [PATCH 03/20] Add missing admin scopes (fix #20892) (#20918) --- config/initializers/doorkeeper.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 84b649f5c..43aac5769 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -98,9 +98,19 @@ Doorkeeper.configure do :'admin:read', :'admin:read:accounts', :'admin:read:reports', + :'admin:read:domain_allows', + :'admin:read:domain_blocks', + :'admin:read:ip_blocks', + :'admin:read:email_domain_blocks', + :'admin:read:canonical_email_blocks', :'admin:write', :'admin:write:accounts', :'admin:write:reports', + :'admin:write:domain_allows', + :'admin:write:domain_blocks', + :'admin:write:ip_blocks', + :'admin:write:email_domain_blocks', + :'admin:write:canonical_email_blocks', :crypto # Change the way client credentials are retrieved from the request object. From 413481f9531411497e0c70f16815bb7e75922e4c Mon Sep 17 00:00:00 2001 From: Chris Johnson <49479599+workeffortwaste@users.noreply.github.com> Date: Thu, 17 Nov 2022 09:52:30 +0000 Subject: [PATCH 04/20] Add maskable icon support for Android (#20904) * Add maskable icon support for Android * Update manifest_serializer.rb * Fix linting issue --- app/serializers/manifest_serializer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/serializers/manifest_serializer.rb b/app/serializers/manifest_serializer.rb index 5604325be..48f3aa7a6 100644 --- a/app/serializers/manifest_serializer.rb +++ b/app/serializers/manifest_serializer.rb @@ -35,6 +35,7 @@ class ManifestSerializer < ActiveModel::Serializer src: full_pack_url("media/icons/android-chrome-#{size}x#{size}.png"), sizes: "#{size}x#{size}", type: 'image/png', + purpose: 'any maskable', } end end From 0cc77263fc4aba44a3c1277ffe617b89d083cfce Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 10:52:51 +0100 Subject: [PATCH 05/20] Change batch account suspension to create a strike (#20897) --- app/models/form/account_batch.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/form/account_batch.rb b/app/models/form/account_batch.rb index 5cfcf7205..473622edf 100644 --- a/app/models/form/account_batch.rb +++ b/app/models/form/account_batch.rb @@ -115,6 +115,10 @@ class Form::AccountBatch authorize(account, :suspend?) log_action(:suspend, account) account.suspend!(origin: :local) + account.strikes.create!( + account: current_account, + action: :suspend + ) Admin::SuspensionWorker.perform_async(account.id) end From 642870c82ba33302ac03981c73bb8e57f03be1e4 Mon Sep 17 00:00:00 2001 From: Alex Nordlund Date: Thu, 17 Nov 2022 10:53:04 +0100 Subject: [PATCH 06/20] Bump Helm chart version to account for mastodon 4 (#20886) --- chart/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chart/Chart.yaml b/chart/Chart.yaml index 7d5bd3c91..8d67e55eb 100644 --- a/chart/Chart.yaml +++ b/chart/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 2.3.0 +version: 3.0.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to From 654d348aac804b3f5f96f21399118f625121501f Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Thu, 17 Nov 2022 10:53:38 +0100 Subject: [PATCH 07/20] Make the button that expands the publish form differentiable from the button that publishes a post (#20864) --- app/javascript/mastodon/features/ui/components/header.js | 2 +- app/javascript/mastodon/locales/defaultMessages.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/ui/components/header.js b/app/javascript/mastodon/features/ui/components/header.js index 4e109080e..bbb0ca1c6 100644 --- a/app/javascript/mastodon/features/ui/components/header.js +++ b/app/javascript/mastodon/features/ui/components/header.js @@ -35,7 +35,7 @@ class Header extends React.PureComponent { if (signedIn) { content = ( <> - {location.pathname !== '/publish' && } + {location.pathname !== '/publish' && } ); diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index f7ea661d7..2b99ac502 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -3989,7 +3989,7 @@ "descriptors": [ { "defaultMessage": "Publish", - "id": "compose_form.publish" + "id": "compose_form.publish_form" }, { "defaultMessage": "Sign in", From e1f819fd78c0c004c0629530203353fbf6d11a91 Mon Sep 17 00:00:00 2001 From: trwnh Date: Thu, 17 Nov 2022 03:54:10 -0600 Subject: [PATCH 08/20] Fix pagination of followed tags (#20861) * Fix missing pagination headers on followed tags * Fix typo --- app/controllers/api/v1/followed_tags_controller.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v1/followed_tags_controller.rb b/app/controllers/api/v1/followed_tags_controller.rb index f0dfd044c..eae2bdc01 100644 --- a/app/controllers/api/v1/followed_tags_controller.rb +++ b/app/controllers/api/v1/followed_tags_controller.rb @@ -3,11 +3,11 @@ class Api::V1::FollowedTagsController < Api::BaseController TAGS_LIMIT = 100 - before_action -> { doorkeeper_authorize! :follow, :read, :'read:follows' }, except: :show + before_action -> { doorkeeper_authorize! :follow, :read, :'read:follows' } before_action :require_user! before_action :set_results - after_action :insert_pagination_headers, only: :show + after_action :insert_pagination_headers def index render json: @results.map(&:tag), each_serializer: REST::TagSerializer, relationships: TagRelationshipsPresenter.new(@results.map(&:tag), current_user&.account_id) @@ -43,7 +43,7 @@ class Api::V1::FollowedTagsController < Api::BaseController end def records_continue? - @results.size == limit_param(TAG_LIMIT) + @results.size == limit_param(TAGS_LIMIT) end def pagination_params(core_params) From eb80789b0bdfe0189d4352c77a8fcfa6e40ff164 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 10:54:33 +0100 Subject: [PATCH 09/20] Fix misleading wording about waitlists (#20850) --- config/locales/en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 679e356b4..048c0f682 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -914,7 +914,7 @@ en: warning: Be very careful with this data. Never share it with anyone! your_token: Your access token auth: - apply_for_account: Get on waitlist + apply_for_account: Request an account change_password: Password delete_account: Delete account delete_account_html: If you wish to delete your account, you can
proceed here. You will be asked for confirmation. From 7955d4b9592a099a8da3374175461b3aa3057c61 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 10:55:03 +0100 Subject: [PATCH 10/20] Add form-action CSP directive (#20781) --- config/initializers/content_security_policy.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 6b62e6f33..cb5629337 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -26,6 +26,7 @@ Rails.application.config.content_security_policy do |p| p.media_src :self, :https, :data, assets_host p.frame_src :self, :https p.manifest_src :self, assets_host + p.form_action :self if Rails.env.development? webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" } From 00b2720ef08e91bee88cd24da7eb2ba836a7a10f Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 10:55:23 +0100 Subject: [PATCH 11/20] Change automatic post deletion configuration to be accessible to redirected users (#20774) Fixes #20550 --- app/controllers/statuses_cleanup_controller.rb | 4 ++++ app/models/user.rb | 6 +++++- config/navigation.rb | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index be234cdcb..e912967fd 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -19,6 +19,10 @@ class StatusesCleanupController < ApplicationController # Do nothing end + def require_functional! + redirect_to edit_user_registration_path unless current_user.functional_or_moved? + end + private def set_policy diff --git a/app/models/user.rb b/app/models/user.rb index 6d566b1c2..3d0298927 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -237,7 +237,11 @@ class User < ApplicationRecord end def functional? - confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? && account.moved_to_account_id.nil? + functional_or_moved? && account.moved_to_account_id.nil? + end + + def functional_or_moved? + confirmed? && approved? && !disabled? && !account.suspended? && !account.memorial? end def unconfirmed? diff --git a/config/navigation.rb b/config/navigation.rb index e901fb932..30817d025 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -17,7 +17,7 @@ SimpleNavigation::Configuration.run do |navigation| n.item :relationships, safe_join([fa_icon('users fw'), t('settings.relationships')]), relationships_path, if: -> { current_user.functional? } n.item :filters, safe_join([fa_icon('filter fw'), t('filters.index.title')]), filters_path, highlights_on: %r{/filters}, if: -> { current_user.functional? } - n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional? } + n.item :statuses_cleanup, safe_join([fa_icon('history fw'), t('settings.statuses_cleanup')]), statuses_cleanup_path, if: -> { current_user.functional_or_moved? } n.item :security, safe_join([fa_icon('lock fw'), t('settings.account')]), edit_user_registration_path do |s| s.item :password, safe_join([fa_icon('lock fw'), t('settings.account_settings')]), edit_user_registration_path, highlights_on: %r{/auth/edit|/settings/delete|/settings/migration|/settings/aliases|/settings/login_activities|^/disputes} From 72618ebf038cceead08a2f6233c2cdbbe04f8f37 Mon Sep 17 00:00:00 2001 From: trwnh Date: Thu, 17 Nov 2022 03:55:50 -0600 Subject: [PATCH 12/20] Fix getting a single EmailDomainBlock (#20846) --- app/policies/email_domain_block_policy.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/policies/email_domain_block_policy.rb b/app/policies/email_domain_block_policy.rb index 1a0ddfa87..0d167ea3e 100644 --- a/app/policies/email_domain_block_policy.rb +++ b/app/policies/email_domain_block_policy.rb @@ -5,6 +5,10 @@ class EmailDomainBlockPolicy < ApplicationPolicy role.can?(:manage_blocks) end + def show? + role.can?(:manage_blocks) + end + def create? role.can?(:manage_blocks) end From 7fdeed5fbca1b6b761029870f02a3f812288f0aa Mon Sep 17 00:00:00 2001 From: trwnh Date: Thu, 17 Nov 2022 03:55:59 -0600 Subject: [PATCH 13/20] Make tag following idempotent (#20860) --- app/controllers/api/v1/tags_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/tags_controller.rb b/app/controllers/api/v1/tags_controller.rb index 32f71bdce..0966ee469 100644 --- a/app/controllers/api/v1/tags_controller.rb +++ b/app/controllers/api/v1/tags_controller.rb @@ -12,7 +12,7 @@ class Api::V1::TagsController < Api::BaseController end def follow - TagFollow.create!(tag: @tag, account: current_account, rate_limit: true) + TagFollow.first_or_create!(tag: @tag, account: current_account, rate_limit: true) render json: @tag, serializer: REST::TagSerializer end From cbb0153bd0945a6aaf612850e1fa6c788336c01b Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 10:58:33 +0100 Subject: [PATCH 14/20] Fix invalid/empty RSS feed link on account pages (#20772) Fixes #20770 --- app/controllers/accounts_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 5ceea5d3c..56229fd05 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -17,6 +17,8 @@ class AccountsController < ApplicationController respond_to do |format| format.html do expires_in 0, public: true unless user_signed_in? + + @rss_url = rss_url end format.rss do From daf6f3453e2a37db3d9a8362d64106b6c7cf0763 Mon Sep 17 00:00:00 2001 From: Joshua Wood Date: Thu, 17 Nov 2022 01:59:35 -0800 Subject: [PATCH 15/20] Handle links with no href in VerifyLinkService (#20741) Before this change, the following error would cause VerifyAccountLinksWorker to fail: NoMethodError: undefined method `downcase' for nil:NilClass [PROJECT_ROOT]/app/services/verify_link_service.rb:31 :in `block in link_back_present?` --- app/services/verify_link_service.rb | 4 +++- spec/services/verify_link_service_spec.rb | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/services/verify_link_service.rb b/app/services/verify_link_service.rb index 0a39d7f26..7496fe2d5 100644 --- a/app/services/verify_link_service.rb +++ b/app/services/verify_link_service.rb @@ -28,7 +28,7 @@ class VerifyLinkService < BaseService links = Nokogiri::HTML(@body).xpath('//a[contains(concat(" ", normalize-space(@rel), " "), " me ")]|//link[contains(concat(" ", normalize-space(@rel), " "), " me ")]') - if links.any? { |link| link['href'].downcase == @link_back.downcase } + if links.any? { |link| link['href']&.downcase == @link_back.downcase } true elsif links.empty? false @@ -38,6 +38,8 @@ class VerifyLinkService < BaseService end def link_redirects_back?(test_url) + return false if test_url.blank? + redirect_to_url = Request.new(:head, test_url, follow: false).perform do |res| res.headers['Location'] end diff --git a/spec/services/verify_link_service_spec.rb b/spec/services/verify_link_service_spec.rb index 3fc88e60e..52ba454cc 100644 --- a/spec/services/verify_link_service_spec.rb +++ b/spec/services/verify_link_service_spec.rb @@ -76,7 +76,25 @@ RSpec.describe VerifyLinkService, type: :service do context 'when a link does not contain a link back' do let(:html) { '' } - it 'marks the field as verified' do + it 'does not mark the field as verified' do + expect(field.verified?).to be false + end + end + + context 'when link has no `href` attribute' do + let(:html) do + <<-HTML + + + + + + Follow me on Mastodon + + HTML + end + + it 'does not mark the field as verified' do expect(field.verified?).to be false end end From 92734e3df167fd3001809a236781a277ec18fe9b Mon Sep 17 00:00:00 2001 From: "Kohei Ota (inductor)" Date: Thu, 17 Nov 2022 19:01:16 +0900 Subject: [PATCH 16/20] Use buildx functions for faster build (#20692) * Use buildx functions for faster build * move link * cannot use --link with --chown --- .github/workflows/build-image.yml | 5 +++-- Dockerfile | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 39fe1bd0b..6c12bd073 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -40,7 +40,8 @@ jobs: with: context: . platforms: linux/amd64,linux/arm64 + builder: ${{ steps.buildx.outputs.name }} push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - cache-from: type=registry,ref=tootsuite/mastodon:edge - cache-to: type=inline + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index cf311fef2..57274cfd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,4 @@ +# syntax=docker/dockerfile:1.4 FROM ubuntu:20.04 as build-dep # Use bash for the shell @@ -65,8 +66,8 @@ RUN cd /opt/mastodon && \ FROM ubuntu:20.04 # Copy over all the langs needed for runtime -COPY --from=build-dep /opt/node /opt/node -COPY --from=build-dep /opt/ruby /opt/ruby +COPY --from=build-dep --link /opt/node /opt/node +COPY --from=build-dep --link /opt/ruby /opt/ruby # Add more PATHs to the PATH ENV PATH="${PATH}:/opt/ruby/bin:/opt/node/bin:/opt/mastodon/bin" From e7deea62d160850025fd551254d013913e8a98ff Mon Sep 17 00:00:00 2001 From: Ell Bradshaw Date: Thu, 17 Nov 2022 02:01:51 -0800 Subject: [PATCH 17/20] Remove last references to "silencing" in UI text (#20637) * Remove last references to "silencing" in en and en_GB locales * Remove stray the, rephrase a bit * Revert changes to generated files I assume these will get updated via Crowdin --- config/locales/en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index 048c0f682..3a80c125d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -386,9 +386,9 @@ en: create: Create block hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. severity: - desc_html: "Silence will make the account's posts invisible to anyone who isn't following them. Suspend will remove all of the account's content, media, and profile data. Use None if you just want to reject media files." + desc_html: "Limit will make posts from accounts at this domain invisible to anyone who isn't following them. Suspend will remove all content, media, and profile data for this domain's accounts from your server. Use None if you just want to reject media files." noop: None - silence: Silence + silence: Limit suspend: Suspend title: New domain block obfuscate: Obfuscate domain name From c373148b3d43056c242fbb891510f1f841ca2f45 Mon Sep 17 00:00:00 2001 From: lenore gilbert Date: Thu, 17 Nov 2022 03:05:09 -0700 Subject: [PATCH 18/20] Support for import/export of instance-level domain blocks/allows for 4.x w/ additional fixes (#20597) * Allow import/export of instance-level domain blocks/allows (#1754) * Allow import/export of instance-level domain blocks/allows. Fixes #15095 * Pacify circleci * Address simple code review feedback * Add headers to exported CSV * Extract common import/export functionality to AdminExportControllerConcern * Add additional fields to instance-blocked domain export * Address review feedback * Split instance domain block/allow import/export into separate pages/controllers * Address code review feedback * Pacify DeepSource * Work around Paperclip::HasAttachmentFile for Rails 6 * Fix deprecated API warning in export tests * Remove after_commit workaround (cherry picked from commit 94e98864e39c010635e839fea984f2b4893bef1a) * Add confirmation page when importing blocked domains (#1773) * Move glitch-soc-specific strings to glitch-soc-specific locale files * Add confirmation page when importing blocked domains (cherry picked from commit b91196f4b73fff91997b8077619ae25b6d04a59e) * Fix authorization check in domain blocks controller (cherry picked from commit 75279377583c6e2aa04cc8d7380c593979630b38) * Fix error strings for domain blocks and email-domain blocks Corrected issue with non-error message used for Mastodon:NotPermittedError in Domain Blocks Corrected issue Domain Blocks using the Email Domain Blocks message on ActionContoller::ParameterMissing Corrected issue with Email Domain Blocks using the not_permitted string from "custom emojii's" * Ran i18n-tasks normalize to address test failure * Removed unused admin.export_domain_blocks.not_permitted string Removing unused string as indicated by Check i18n * Fix tests (cherry picked from commit 9094c2f52c24e1c00b594e7c11cd00e4a07eb431) * Fix domain block export not exporting blocks with only media rejection (cherry picked from commit 26ff48ee48a5c03a2a4b0bd03fd322529e6bd960) * Fix various issues with domain block import - stop using Paperclip for processing domain allow/block imports - stop leaving temporary files - better error handling - assume CSV files are UTF-8-encoded (cherry picked from commit cad824d8f501b95377e4f0a957e5a00d517a1902) Co-authored-by: Levi Bard Co-authored-by: Claire --- .../admin/domain_blocks_controller.rb | 22 ++++++ .../admin/email_domain_blocks_controller.rb | 2 +- .../admin/export_domain_allows_controller.rb | 60 ++++++++++++++++ .../admin/export_domain_blocks_controller.rb | 71 +++++++++++++++++++ .../admin_export_controller_concern.rb | 39 ++++++++++ app/javascript/packs/admin.js | 6 ++ app/models/admin/import.rb | 32 +++++++++ app/models/domain_allow.rb | 4 ++ app/models/domain_block.rb | 1 + app/models/form/domain_block_batch.rb | 35 +++++++++ .../admin/export_domain_allows/new.html.haml | 10 +++ .../_domain_block.html.haml | 27 +++++++ .../export_domain_blocks/import.html.haml | 21 ++++++ .../admin/export_domain_blocks/new.html.haml | 10 +++ app/views/admin/instances/index.html.haml | 4 ++ config/locales/en.yml | 21 ++++++ config/routes.rb | 21 +++++- .../admin/domain_allows_controller_spec.rb | 48 +++++++++++++ .../admin/domain_blocks_controller_spec.rb | 21 ++++++ .../export_domain_allows_controller_spec.rb | 42 +++++++++++ .../export_domain_blocks_controller_spec.rb | 35 +++++++++ spec/fixtures/files/domain_allows.csv | 3 + spec/fixtures/files/domain_blocks.csv | 4 ++ 23 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 app/controllers/admin/export_domain_allows_controller.rb create mode 100644 app/controllers/admin/export_domain_blocks_controller.rb create mode 100644 app/controllers/concerns/admin_export_controller_concern.rb create mode 100644 app/models/admin/import.rb create mode 100644 app/models/form/domain_block_batch.rb create mode 100644 app/views/admin/export_domain_allows/new.html.haml create mode 100644 app/views/admin/export_domain_blocks/_domain_block.html.haml create mode 100644 app/views/admin/export_domain_blocks/import.html.haml create mode 100644 app/views/admin/export_domain_blocks/new.html.haml create mode 100644 spec/controllers/admin/domain_allows_controller_spec.rb create mode 100644 spec/controllers/admin/export_domain_allows_controller_spec.rb create mode 100644 spec/controllers/admin/export_domain_blocks_controller_spec.rb create mode 100644 spec/fixtures/files/domain_allows.csv create mode 100644 spec/fixtures/files/domain_blocks.csv diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 16defc1ea..e79f7a43e 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -4,6 +4,18 @@ module Admin class DomainBlocksController < BaseController before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + def batch + authorize :domain_block, :create? + @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.domain_blocks.no_domain_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.domain_blocks.not_permitted') + else + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + end + def new authorize :domain_block, :create? @domain_block = DomainBlock.new(domain: params[:_domain]) @@ -76,5 +88,15 @@ module Admin def resource_params params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) end + + def form_domain_block_batch_params + params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate]) + end + + def action_from_button + if params[:save] + 'save' + end + end end end diff --git a/app/controllers/admin/email_domain_blocks_controller.rb b/app/controllers/admin/email_domain_blocks_controller.rb index 593457b94..a0a43de19 100644 --- a/app/controllers/admin/email_domain_blocks_controller.rb +++ b/app/controllers/admin/email_domain_blocks_controller.rb @@ -19,7 +19,7 @@ module Admin rescue ActionController::ParameterMissing flash[:alert] = I18n.t('admin.email_domain_blocks.no_email_domain_block_selected') rescue Mastodon::NotPermittedError - flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') + flash[:alert] = I18n.t('admin.email_domain_blocks.not_permitted') ensure redirect_to admin_email_domain_blocks_path end diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb new file mode 100644 index 000000000..57fb12c62 --- /dev/null +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainAllowsController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + def new + authorize :domain_allow, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_allow, :create? + begin + @import = Admin::Import.new(import_params) + return render :new unless @import.validate + + parse_import_data!(export_headers) + + @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).each do |row| + domain = row['#domain'].strip + next if DomainAllow.allowed?(domain) + + domain_allow = DomainAllow.new(domain: domain) + log_action :create, domain_allow if domain_allow.save + end + flash[:notice] = I18n.t('admin.domain_allows.created_msg') + rescue ActionController::ParameterMissing + flash[:error] = I18n.t('admin.export_domain_allows.no_file') + end + redirect_to admin_instances_path + end + + private + + def export_filename + 'domain_allows.csv' + end + + def export_headers + %w(#domain) + end + + def export_data + CSV.generate(headers: export_headers, write_headers: true) do |content| + DomainAllow.allowed_domains.each do |instance| + content << [instance.domain] + end + end + end + end +end diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb new file mode 100644 index 000000000..fb0cd05d2 --- /dev/null +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainBlocksController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + def new + authorize :domain_block, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_block, :create? + + @import = Admin::Import.new(import_params) + return render :new unless @import.validate + + parse_import_data!(export_headers) + + @global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc)) + + @form = Form::DomainBlockBatch.new + @domain_blocks = @data.take(Admin::Import::ROWS_PROCESSING_LIMIT).filter_map do |row| + domain = row['#domain'].strip + next if DomainBlock.rule_for(domain).present? + + domain_block = DomainBlock.new(domain: domain, + severity: row['#severity'].strip, + reject_media: row['#reject_media'].strip, + reject_reports: row['#reject_reports'].strip, + private_comment: @global_private_comment, + public_comment: row['#public_comment']&.strip, + obfuscate: row['#obfuscate'].strip) + + domain_block if domain_block.valid? + end + + @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + rescue ActionController::ParameterMissing + flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') + set_dummy_import! + render :new + end + + private + + def export_filename + 'domain_blocks.csv' + end + + def export_headers + %w(#domain #severity #reject_media #reject_reports #public_comment #obfuscate) + end + + def export_data + CSV.generate(headers: export_headers, write_headers: true) do |content| + DomainBlock.with_limitations.each do |instance| + content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] + end + end + end + end +end diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin_export_controller_concern.rb new file mode 100644 index 000000000..b40c76557 --- /dev/null +++ b/app/controllers/concerns/admin_export_controller_concern.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module AdminExportControllerConcern + extend ActiveSupport::Concern + + private + + def send_export_file + respond_to do |format| + format.csv { send_data export_data, filename: export_filename } + end + end + + def export_data + raise 'Override in controller' + end + + def export_filename + raise 'Override in controller' + end + + def set_dummy_import! + @import = Admin::Import.new + end + + def import_params + params.require(:admin_import).permit(:data) + end + + def import_data_path + params[:admin_import][:data].path + end + + def parse_import_data!(default_headers) + data = CSV.read(import_data_path, headers: true, encoding: 'UTF-8') + data = CSV.read(import_data_path, headers: default_headers, encoding: 'UTF-8') unless data.headers&.first&.strip&.include?(default_headers[0]) + @data = data.reject(&:blank?) + end +end diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js index de86e0e11..4e817129d 100644 --- a/app/javascript/packs/admin.js +++ b/app/javascript/packs/admin.js @@ -185,6 +185,12 @@ ready(() => { const registrationMode = document.getElementById('form_admin_settings_registrations_mode'); if (registrationMode) onChangeRegistrationMode(registrationMode); + const checkAllElement = document.querySelector('#batch_checkbox_all'); + if (checkAllElement) { + checkAllElement.checked = [].every.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + checkAllElement.indeterminate = !checkAllElement.checked && [].some.call(document.querySelectorAll(batchCheckboxClassName), (content) => content.checked); + } + document.querySelector('a#add-instance-button')?.addEventListener('click', (e) => { const domain = document.getElementById('by_domain')?.value; diff --git a/app/models/admin/import.rb b/app/models/admin/import.rb new file mode 100644 index 000000000..79c0722d5 --- /dev/null +++ b/app/models/admin/import.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# A non-activerecord helper class for csv upload +class Admin::Import + include ActiveModel::Model + + ROWS_PROCESSING_LIMIT = 20_000 + + attr_accessor :data + + validates :data, presence: true + validate :validate_data + + def data_file_name + data.original_filename + end + + private + + def validate_data + return if data.blank? + + csv_data = CSV.read(data.path, encoding: 'UTF-8') + + row_count = csv_data.size + row_count -= 1 if csv_data.first&.first == '#domain' + + errors.add(:data, I18n.t('imports.errors.over_rows_processing_limit', count: ROWS_PROCESSING_LIMIT)) if row_count > ROWS_PROCESSING_LIMIT + rescue CSV::MalformedCSVError => e + errors.add(:data, I18n.t('imports.errors.invalid_csv_file', error: e.message)) + end +end diff --git a/app/models/domain_allow.rb b/app/models/domain_allow.rb index 65f494fed..9e746b915 100644 --- a/app/models/domain_allow.rb +++ b/app/models/domain_allow.rb @@ -28,6 +28,10 @@ class DomainAllow < ApplicationRecord !rule_for(domain).nil? end + def allowed_domains + select(:domain) + end + def rule_for(domain) return if domain.blank? diff --git a/app/models/domain_block.rb b/app/models/domain_block.rb index ad1dc2a38..8e298ac9d 100644 --- a/app/models/domain_block.rb +++ b/app/models/domain_block.rb @@ -29,6 +29,7 @@ class DomainBlock < ApplicationRecord scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) } scope :with_user_facing_limitations, -> { where(severity: [:silence, :suspend]) } + scope :with_limitations, -> { where(severity: [:silence, :suspend]).or(where(reject_media: true)) } scope :by_severity, -> { order(Arel.sql('(CASE severity WHEN 0 THEN 1 WHEN 1 THEN 2 WHEN 2 THEN 0 END), domain')) } def to_log_human_identifier diff --git a/app/models/form/domain_block_batch.rb b/app/models/form/domain_block_batch.rb new file mode 100644 index 000000000..39012df51 --- /dev/null +++ b/app/models/form/domain_block_batch.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class Form::DomainBlockBatch + include ActiveModel::Model + include Authorization + include AccountableConcern + + attr_accessor :domain_blocks_attributes, :action, :current_account + + def save + case action + when 'save' + save! + end + end + + private + + def domain_blocks + @domain_blocks ||= domain_blocks_attributes.values.filter_map do |attributes| + DomainBlock.new(attributes.without('enabled')) if ActiveModel::Type::Boolean.new.cast(attributes['enabled']) + end + end + + def save! + domain_blocks.each do |domain_block| + authorize(domain_block, :create?) + next if DomainBlock.rule_for(domain_block.domain).present? + + domain_block.save! + DomainBlockWorker.perform_async(domain_block.id) + log_action :create, domain_block + end + end +end diff --git a/app/views/admin/export_domain_allows/new.html.haml b/app/views/admin/export_domain_allows/new.html.haml new file mode 100644 index 000000000..dc0cf8c52 --- /dev/null +++ b/app/views/admin/export_domain_allows/new.html.haml @@ -0,0 +1,10 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @import, url: import_admin_export_domain_allows_path, html: { multipart: true } do |f| + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data'), as: :file + + .actions + = f.button :button, t('imports.upload'), type: :submit diff --git a/app/views/admin/export_domain_blocks/_domain_block.html.haml b/app/views/admin/export_domain_blocks/_domain_block.html.haml new file mode 100644 index 000000000..5d4b6c4d0 --- /dev/null +++ b/app/views/admin/export_domain_blocks/_domain_block.html.haml @@ -0,0 +1,27 @@ +- existing_relationships ||= false + +.batch-table__row{ class: [existing_relationships && 'batch-table__row--attention'] } + %label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox + = f.check_box :enabled, checked: !existing_relationships + .batch-table__row__content.pending-account + .pending-account__header + %strong + = f.object.domain + = f.hidden_field :domain + = f.hidden_field :severity + = f.hidden_field :reject_media + = f.hidden_field :reject_reports + = f.hidden_field :obfuscate + = f.hidden_field :private_comment + = f.hidden_field :public_comment + + %br/ + + = f.object.policies.map { |policy| t(policy, scope: 'admin.instances.content_policies.policies') }.join(' ā€¢ ') + - if f.object.public_comment.present? + ā€¢ + = f.object.public_comment + - if existing_relationships + ā€¢ + = fa_icon 'warning fw' + = t('admin.export_domain_blocks.import.existing_relationships_warning') diff --git a/app/views/admin/export_domain_blocks/import.html.haml b/app/views/admin/export_domain_blocks/import.html.haml new file mode 100644 index 000000000..01add232d --- /dev/null +++ b/app/views/admin/export_domain_blocks/import.html.haml @@ -0,0 +1,21 @@ +- content_for :page_title do + = t('admin.export_domain_blocks.import.title') + +%p= t('admin.export_domain_blocks.import.description_html') + +- if defined?(@global_private_comment) && @global_private_comment.present? + %p= t('admin.export_domain_blocks.import.private_comment_description_html', comment: @global_private_comment) + += form_for(@form, url: batch_admin_domain_blocks_path) do |f| + .batch-table + .batch-table__toolbar + %label.batch-table__toolbar__select.batch-checkbox-all + = check_box_tag :batch_checkbox_all, nil, false + .batch-table__toolbar__actions + = f.button safe_join([fa_icon('copy'), t('admin.domain_blocks.import')]), name: :save, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } + .batch-table__body + - if @domain_blocks.empty? + = nothing_here 'nothing-here--under-tabs' + - else + = f.simple_fields_for :domain_blocks, @domain_blocks do |ff| + = render 'domain_block', f: ff, existing_relationships: @warning_domains.include?(ff.object.domain) diff --git a/app/views/admin/export_domain_blocks/new.html.haml b/app/views/admin/export_domain_blocks/new.html.haml new file mode 100644 index 000000000..0291aeed7 --- /dev/null +++ b/app/views/admin/export_domain_blocks/new.html.haml @@ -0,0 +1,10 @@ +- content_for :page_title do + = t('.title') + += simple_form_for @import, url: import_admin_export_domain_blocks_path, html: { multipart: true } do |f| + .fields-row + .fields-group.fields-row__column.fields-row__column-6 + = f.input :data, wrapper: :with_block_label, hint: t('simple_form.hints.imports.data'), as: :file + + .actions + = f.button :button, t('imports.upload'), type: :submit diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index cc5020398..8f7e3e67d 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -7,8 +7,12 @@ - content_for :heading_actions do - if whitelist_mode? = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button', id: 'add-instance-button' + = link_to t('admin.domain_allows.export'), export_admin_export_domain_allows_path(format: :csv), class: 'button' + = link_to t('admin.domain_allows.import'), new_admin_export_domain_allow_path, class: 'button' - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button', id: 'add-instance-button' + = link_to t('admin.domain_blocks.export'), export_admin_export_domain_blocks_path(format: :csv), class: 'button' + = link_to t('admin.domain_blocks.import'), new_admin_export_domain_block_path, class: 'button' .filters .filter-subset diff --git a/config/locales/en.yml b/config/locales/en.yml index 3a80c125d..1cc53dca4 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -373,6 +373,8 @@ en: add_new: Allow federation with domain created_msg: Domain has been successfully allowed for federation destroyed_msg: Domain has been disallowed from federation + export: Export + import: Import undo: Disallow federation with domain domain_blocks: add_new: Add new domain block @@ -382,6 +384,8 @@ en: edit: Edit domain block existing_domain_block: You have already imposed stricter limits on %{name}. existing_domain_block_html: You have already imposed stricter limits on %{name}, you need to unblock it first. + export: Export + import: Import new: create: Create block hint: The domain block will not prevent creation of account entries in the database, but will retroactively and automatically apply specific moderation methods on those accounts. @@ -391,6 +395,8 @@ en: silence: Limit suspend: Suspend title: New domain block + no_domain_block_selected: No domain blocks were changed as none were selected + not_permitted: You are not permitted to perform this action obfuscate: Obfuscate domain name obfuscate_hint: Partially obfuscate the domain name in the list if advertising the list of domain limitations is enabled private_comment: Private comment @@ -422,6 +428,20 @@ en: resolved_dns_records_hint_html: The domain name resolves to the following MX domains, which are ultimately responsible for accepting e-mail. Blocking an MX domain will block sign-ups from any e-mail address which uses the same MX domain, even if the visible domain name is different. Be careful not to block major e-mail providers. resolved_through_html: Resolved through %{domain} title: Blocked e-mail domains + export_domain_allows: + new: + title: Import domain allows + no_file: No file selected + export_domain_blocks: + import: + description_html: You are about to import a list of domain blocks. Please review this list very carefully, especially if you have not authored this list yourself. + existing_relationships_warning: Existing follow relationships + private_comment_description_html: 'To help you track where imported blocks come from, imported blocks will be created with the following private comment: %{comment}' + private_comment_template: Imported from %{source} on %{date} + title: Import domain blocks + new: + title: Import domain blocks + no_file: No file selected follow_recommendations: description_html: "Follow recommendations help new users quickly find interesting content. When a user has not interacted with others enough to form personalized follow recommendations, these accounts are recommended instead. They are re-calculated on a daily basis from a mix of accounts with the highest recent engagements and highest local follower counts for a given language." language: For language @@ -1159,6 +1179,7 @@ en: invalid_markup: 'contains invalid HTML markup: %{error}' imports: errors: + invalid_csv_file: 'Invalid CSV file. Error: %{error}' over_rows_processing_limit: contains more than %{count} rows modes: merge: Merge diff --git a/config/routes.rb b/config/routes.rb index 98aa5a033..98e19667c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -225,7 +225,25 @@ Rails.application.routes.draw do get '/dashboard', to: 'dashboard#index' resources :domain_allows, only: [:new, :create, :show, :destroy] - resources :domain_blocks, only: [:new, :create, :destroy, :update, :edit] + resources :domain_blocks, only: [:new, :create, :show, :destroy, :update, :edit] do + collection do + post :batch + end + end + + resources :export_domain_allows, only: [:new] do + collection do + get :export, constraints: { format: :csv } + post :import + end + end + + resources :export_domain_blocks, only: [:new] do + collection do + get :export, constraints: { format: :csv } + post :import + end + end resources :email_domain_blocks, only: [:index, :new, :create] do collection do @@ -523,6 +541,7 @@ Rails.application.routes.draw do end resource :domain_blocks, only: [:show, :create, :destroy] + resource :directory, only: [:show] resources :follow_requests, only: [:index] do diff --git a/spec/controllers/admin/domain_allows_controller_spec.rb b/spec/controllers/admin/domain_allows_controller_spec.rb new file mode 100644 index 000000000..6c4e67787 --- /dev/null +++ b/spec/controllers/admin/domain_allows_controller_spec.rb @@ -0,0 +1,48 @@ +require 'rails_helper' + +RSpec.describe Admin::DomainAllowsController, type: :controller do + render_views + + before do + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user + end + + describe 'GET #new' do + it 'assigns a new domain allow' do + get :new + + expect(assigns(:domain_allow)).to be_instance_of(DomainAllow) + expect(response).to have_http_status(200) + end + end + + describe 'POST #create' do + it 'blocks the domain when succeeded to save' do + post :create, params: { domain_allow: { domain: 'example.com' } } + + expect(flash[:notice]).to eq I18n.t('admin.domain_allows.created_msg') + expect(response).to redirect_to(admin_instances_path) + end + + it 'renders new when failed to save' do + Fabricate(:domain_allow, domain: 'example.com') + + post :create, params: { domain_allow: { domain: 'example.com' } } + + expect(response).to render_template :new + end + end + + describe 'DELETE #destroy' do + it 'disallows the domain' do + service = double(call: true) + allow(UnallowDomainService).to receive(:new).and_return(service) + domain_allow = Fabricate(:domain_allow) + delete :destroy, params: { id: domain_allow.id } + + expect(service).to have_received(:call).with(domain_allow) + expect(flash[:notice]).to eq I18n.t('admin.domain_allows.destroyed_msg') + expect(response).to redirect_to(admin_instances_path) + end + end +end diff --git a/spec/controllers/admin/domain_blocks_controller_spec.rb b/spec/controllers/admin/domain_blocks_controller_spec.rb index 5c2dcd268..98cda5004 100644 --- a/spec/controllers/admin/domain_blocks_controller_spec.rb +++ b/spec/controllers/admin/domain_blocks_controller_spec.rb @@ -16,6 +16,27 @@ RSpec.describe Admin::DomainBlocksController, type: :controller do end end + describe 'POST #batch' do + it 'blocks the domains when succeeded to save' do + allow(DomainBlockWorker).to receive(:perform_async).and_return(true) + + post :batch, params: { + save: '', + form_domain_block_batch: { + domain_blocks_attributes: { + '0' => { enabled: '1', domain: 'example.com', severity: 'silence' }, + '1' => { enabled: '0', domain: 'mastodon.social', severity: 'suspend' }, + '2' => { enabled: '1', domain: 'mastodon.online', severity: 'suspend' } + } + } + } + + expect(DomainBlockWorker).to have_received(:perform_async).exactly(2).times + expect(flash[:notice]).to eq I18n.t('admin.domain_blocks.created_msg') + expect(response).to redirect_to(admin_instances_path(limited: '1')) + end + end + describe 'POST #create' do it 'blocks the domain when succeeded to save' do allow(DomainBlockWorker).to receive(:perform_async).and_return(true) diff --git a/spec/controllers/admin/export_domain_allows_controller_spec.rb b/spec/controllers/admin/export_domain_allows_controller_spec.rb new file mode 100644 index 000000000..1e1a5ae7d --- /dev/null +++ b/spec/controllers/admin/export_domain_allows_controller_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +RSpec.describe Admin::ExportDomainAllowsController, type: :controller do + render_views + + before do + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user + end + + describe 'GET #export' do + it 'renders instances' do + Fabricate(:domain_allow, domain: 'good.domain') + Fabricate(:domain_allow, domain: 'better.domain') + + get :export, params: { format: :csv } + expect(response).to have_http_status(200) + expect(response.body).to eq(IO.read(File.join(file_fixture_path, 'domain_allows.csv'))) + end + end + + describe 'POST #import' do + it 'allows imported domains' do + post :import, params: { admin_import: { data: fixture_file_upload('domain_allows.csv') } } + + expect(response).to redirect_to(admin_instances_path) + + # Header should not be imported + expect(DomainAllow.where(domain: '#domain').present?).to eq(false) + + # Domains should now be added + get :export, params: { format: :csv } + expect(response).to have_http_status(200) + expect(response.body).to eq(IO.read(File.join(file_fixture_path, 'domain_allows.csv'))) + end + + it 'displays error on no file selected' do + post :import, params: { admin_import: {} } + expect(response).to redirect_to(admin_instances_path) + expect(flash[:error]).to eq(I18n.t('admin.export_domain_allows.no_file')) + end + end +end diff --git a/spec/controllers/admin/export_domain_blocks_controller_spec.rb b/spec/controllers/admin/export_domain_blocks_controller_spec.rb new file mode 100644 index 000000000..8697e0c21 --- /dev/null +++ b/spec/controllers/admin/export_domain_blocks_controller_spec.rb @@ -0,0 +1,35 @@ +require 'rails_helper' + +RSpec.describe Admin::ExportDomainBlocksController, type: :controller do + render_views + + before do + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Admin')), scope: :user + end + + describe 'GET #export' do + it 'renders instances' do + Fabricate(:domain_block, domain: 'bad.domain', severity: 'silence', public_comment: 'bad') + Fabricate(:domain_block, domain: 'worse.domain', severity: 'suspend', reject_media: true, reject_reports: true, public_comment: 'worse', obfuscate: true) + Fabricate(:domain_block, domain: 'reject.media', severity: 'noop', reject_media: true, public_comment: 'reject media') + Fabricate(:domain_block, domain: 'no.op', severity: 'noop', public_comment: 'noop') + + get :export, params: { format: :csv } + expect(response).to have_http_status(200) + expect(response.body).to eq(IO.read(File.join(file_fixture_path, 'domain_blocks.csv'))) + end + end + + describe 'POST #import' do + it 'blocks imported domains' do + post :import, params: { admin_import: { data: fixture_file_upload('domain_blocks.csv') } } + + expect(assigns(:domain_blocks).map(&:domain)).to match_array ['bad.domain', 'worse.domain', 'reject.media'] + end + end + + it 'displays error on no file selected' do + post :import, params: { admin_import: {} } + expect(flash[:alert]).to eq(I18n.t('admin.export_domain_blocks.no_file')) + end +end diff --git a/spec/fixtures/files/domain_allows.csv b/spec/fixtures/files/domain_allows.csv new file mode 100644 index 000000000..4200ac3f5 --- /dev/null +++ b/spec/fixtures/files/domain_allows.csv @@ -0,0 +1,3 @@ +#domain +good.domain +better.domain diff --git a/spec/fixtures/files/domain_blocks.csv b/spec/fixtures/files/domain_blocks.csv new file mode 100644 index 000000000..28ffb9175 --- /dev/null +++ b/spec/fixtures/files/domain_blocks.csv @@ -0,0 +1,4 @@ +#domain,#severity,#reject_media,#reject_reports,#public_comment,#obfuscate +bad.domain,silence,false,false,bad,false +worse.domain,suspend,true,true,worse,true +reject.media,noop,true,false,reject media,false From 4f15fd0ba1e992aa6cbe95573372c625a2f4cf71 Mon Sep 17 00:00:00 2001 From: Rose <83477269+AtariDreams@users.noreply.github.com> Date: Thu, 17 Nov 2022 05:05:39 -0500 Subject: [PATCH 19/20] Fix style for hashes (#20518) * Fix style for hashes Make the style for hashes consistent. * New style More consistency --- .rubocop.yml | 4 ++++ app/views/application/_card.html.haml | 2 +- app/views/auth/challenges/new.html.haml | 2 +- app/views/auth/confirmations/new.html.haml | 2 +- app/views/auth/passwords/edit.html.haml | 4 ++-- app/views/auth/passwords/new.html.haml | 2 +- .../auth/registrations/_sessions.html.haml | 2 +- app/views/auth/registrations/edit.html.haml | 8 +++---- app/views/auth/registrations/new.html.haml | 14 +++++------ app/views/auth/sessions/new.html.haml | 6 ++--- .../_otp_authentication_form.html.haml | 2 +- app/views/auth/setup/show.html.haml | 2 +- app/views/settings/deletes/show.html.haml | 4 ++-- .../migration/redirects/new.html.haml | 4 ++-- app/views/settings/migrations/show.html.haml | 4 ++-- .../confirmations/new.html.haml | 2 +- .../webauthn_credentials/new.html.haml | 2 +- app/views/statuses/_detailed_status.html.haml | 4 ++-- app/views/statuses/_simple_status.html.haml | 4 ++-- config/environments/production.rb | 24 +++++++++---------- .../admin/statuses_controller_spec.rb | 2 +- spec/services/favourite_service_spec.rb | 2 +- spec/services/follow_service_spec.rb | 2 +- spec/views/statuses/show.html.haml_spec.rb | 2 +- 24 files changed, 55 insertions(+), 51 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 8dc2d1c47..38a413c2e 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -243,6 +243,10 @@ Style/HashTransformKeys: Style/HashTransformValues: Enabled: false +Style/HashSyntax: + Enabled: true + EnforcedStyle: ruby19_no_mixed_keys + Style/IfUnlessModifier: Enabled: false diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 909d9ff81..3d0e6b1da 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -13,4 +13,4 @@ %strong.emojify.p-name= display_name(account, custom_emojify: true) %span = acct(account) - = fa_icon('lock', { :data => ({hidden: true} unless account.locked?)}) + = fa_icon('lock', { data: ({hidden: true} unless account.locked?)}) diff --git a/app/views/auth/challenges/new.html.haml b/app/views/auth/challenges/new.html.haml index ff4b7a506..4f21e4af6 100644 --- a/app/views/auth/challenges/new.html.haml +++ b/app/views/auth/challenges/new.html.haml @@ -5,7 +5,7 @@ = f.input :return_to, as: :hidden .field-group - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password', :autofocus => true }, label: t('challenge.prompt'), required: true + = f.input :current_password, wrapper: :with_block_label, input_html: { autocomplete: 'current-password', autofocus: true }, label: t('challenge.prompt'), required: true .actions = f.button :button, t('challenge.confirm'), type: :submit diff --git a/app/views/auth/confirmations/new.html.haml b/app/views/auth/confirmations/new.html.haml index 4a1bedaa4..a294d3cb5 100644 --- a/app/views/auth/confirmations/new.html.haml +++ b/app/views/auth/confirmations/new.html.haml @@ -5,7 +5,7 @@ = render 'shared/error_messages', object: resource .fields-group - = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false + = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, hint: false .actions = f.button :button, t('auth.resend_confirmation'), type: :submit diff --git a/app/views/auth/passwords/edit.html.haml b/app/views/auth/passwords/edit.html.haml index c7dbebe75..b95a9b676 100644 --- a/app/views/auth/passwords/edit.html.haml +++ b/app/views/auth/passwords/edit.html.haml @@ -8,9 +8,9 @@ = f.input :reset_password_token, as: :hidden .fields-group - = f.input :password, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, required: true + = f.input :password, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label': t('simple_form.labels.defaults.new_password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, required: true .fields-group - = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'new-password' }, required: true + = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_new_password'), autocomplete: 'new-password' }, required: true .actions = f.button :button, t('auth.set_new_password'), type: :submit diff --git a/app/views/auth/passwords/new.html.haml b/app/views/auth/passwords/new.html.haml index bae5b24ba..10ad108ea 100644 --- a/app/views/auth/passwords/new.html.haml +++ b/app/views/auth/passwords/new.html.haml @@ -5,7 +5,7 @@ = render 'shared/error_messages', object: resource .fields-group - = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false + = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, hint: false .actions = f.button :button, t('auth.reset_password'), type: :submit diff --git a/app/views/auth/registrations/_sessions.html.haml b/app/views/auth/registrations/_sessions.html.haml index 5d993f574..c094dfd25 100644 --- a/app/views/auth/registrations/_sessions.html.haml +++ b/app/views/auth/registrations/_sessions.html.haml @@ -18,7 +18,7 @@ %tr %td %span{ title: session.user_agent }< - = fa_icon "#{session_device_icon(session)} fw", 'aria-label' => session_device_icon(session) + = fa_icon "#{session_device_icon(session)} fw", 'aria-label': session_device_icon(session) = ' ' = t 'sessions.description', browser: t("sessions.browsers.#{session.browser}", default: "#{session.browser}"), platform: t("sessions.platforms.#{session.platform}", default: "#{session.platform}") %td diff --git a/app/views/auth/registrations/edit.html.haml b/app/views/auth/registrations/edit.html.haml index c642c2293..60fd1635e 100644 --- a/app/views/auth/registrations/edit.html.haml +++ b/app/views/auth/registrations/edit.html.haml @@ -11,15 +11,15 @@ - if !use_seamless_external_login? || resource.encrypted_password.present? .fields-row .fields-row__column.fields-group.fields-row__column-6 - = f.input :email, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? + = f.input :email, wrapper: :with_label, input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, required: true, disabled: current_account.suspended? .fields-row__column.fields-group.fields-row__column-6 - = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label' => t('simple_form.labels.defaults.current_password'), :autocomplete => 'current-password' }, required: true, disabled: current_account.suspended?, hint: false + = f.input :current_password, wrapper: :with_label, input_html: { 'aria-label': t('simple_form.labels.defaults.current_password'), autocomplete: 'current-password' }, required: true, disabled: current_account.suspended?, hint: false .fields-row .fields-row__column.fields-group.fields-row__column-6 - = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.new_password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended? + = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.new_password'), input_html: { 'aria-label': t('simple_form.labels.defaults.new_password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, hint: t('simple_form.hints.defaults.password'), disabled: current_account.suspended? .fields-row__column.fields-group.fields-row__column-6 - = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_new_password'), :autocomplete => 'new-password' }, disabled: current_account.suspended? + = f.input :password_confirmation, wrapper: :with_label, label: t('simple_form.labels.defaults.confirm_new_password'), input_html: { 'aria-label': t('simple_form.labels.defaults.confirm_new_password'), autocomplete: 'new-password' }, disabled: current_account.suspended? .actions = f.button :button, t('generic.save_changes'), type: :submit, class: 'button', disabled: current_account.suspended? diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index b1d52dd0c..0d8fd800f 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -17,13 +17,13 @@ .fields-group = f.simple_fields_for :account do |ff| - = ff.input :display_name, wrapper: :with_label, label: false, required: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.display_name'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.display_name') } - = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false - = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'username' }, hint: false - = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'new-password', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false - = 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 => 'new-password' }, hint: false - = 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 - = 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' } + = ff.input :display_name, wrapper: :with_label, label: false, required: false, input_html: { 'aria-label': t('simple_form.labels.defaults.display_name'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.display_name') } + = ff.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.username'), autocomplete: 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false + = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'username' }, hint: false + = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'new-password', minlength: User.password_length.first, maxlength: User.password_length.last }, hint: false + = 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: 'new-password' }, hint: false + = 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 + = 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/auth/sessions/new.html.haml b/app/views/auth/sessions/new.html.haml index 943618e39..304e3ab84 100644 --- a/app/views/auth/sessions/new.html.haml +++ b/app/views/auth/sessions/new.html.haml @@ -8,11 +8,11 @@ = simple_form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| .fields-group - if use_seamless_external_login? - = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.username_or_email') }, hint: false + = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.username_or_email'), input_html: { 'aria-label': t('simple_form.labels.defaults.username_or_email') }, hint: false - else - = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label' => t('simple_form.labels.defaults.email') }, hint: false + = f.input :email, autofocus: true, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), input_html: { 'aria-label': t('simple_form.labels.defaults.email') }, hint: false .fields-group - = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'current-password' }, hint: false + = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), input_html: { 'aria-label': t('simple_form.labels.defaults.password'), autocomplete: 'current-password' }, hint: false .actions = f.button :button, t('auth.login'), type: :submit diff --git a/app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml b/app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml index 82f957527..094b502b1 100644 --- a/app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml +++ b/app/views/auth/sessions/two_factor/_otp_authentication_form.html.haml @@ -5,7 +5,7 @@ %p.hint.authentication-hint= t('simple_form.hints.sessions.otp') .fields-group - = f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'one-time-code' }, autofocus: true + = f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label': t('simple_form.labels.defaults.otp_attempt'), autocomplete: 'one-time-code' }, autofocus: true .actions = f.button :button, t('auth.login'), type: :submit diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml index c14fed56f..1a6611ceb 100644 --- a/app/views/auth/setup/show.html.haml +++ b/app/views/auth/setup/show.html.haml @@ -9,7 +9,7 @@ %p.hint= t('auth.setup.email_below_hint_html') .fields-group - = f.input :email, required: true, hint: false, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } + = f.input :email, required: true, hint: false, input_html: { 'aria-label': t('simple_form.labels.defaults.email'), autocomplete: 'off' } .actions = f.submit t('admin.accounts.change_email.label'), class: 'button' diff --git a/app/views/settings/deletes/show.html.haml b/app/views/settings/deletes/show.html.haml index c08ee85b0..2e9785c89 100644 --- a/app/views/settings/deletes/show.html.haml +++ b/app/views/settings/deletes/show.html.haml @@ -21,9 +21,9 @@ %hr.spacer/ - if current_user.encrypted_password.present? - = f.input :password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, hint: t('deletes.confirm_password') + = f.input :password, wrapper: :with_block_label, input_html: { autocomplete: 'current-password' }, hint: t('deletes.confirm_password') - else - = f.input :username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, hint: t('deletes.confirm_username') + = f.input :username, wrapper: :with_block_label, input_html: { autocomplete: 'off' }, hint: t('deletes.confirm_username') .actions = f.button :button, t('deletes.proceed'), type: :submit, class: 'negative' diff --git a/app/views/settings/migration/redirects/new.html.haml b/app/views/settings/migration/redirects/new.html.haml index d7868e900..370087879 100644 --- a/app/views/settings/migration/redirects/new.html.haml +++ b/app/views/settings/migration/redirects/new.html.haml @@ -19,9 +19,9 @@ .fields-row__column.fields-group.fields-row__column-6 - if current_user.encrypted_password.present? - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, required: true + = f.input :current_password, wrapper: :with_block_label, input_html: { autocomplete: 'current-password' }, required: true - else - = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true + = f.input :current_username, wrapper: :with_block_label, input_html: { autocomplete: 'off' }, required: true .actions = f.button :button, t('migrations.set_redirect'), type: :submit, class: 'button button--destructive' diff --git a/app/views/settings/migrations/show.html.haml b/app/views/settings/migrations/show.html.haml index 1ecf7302a..31f7d5e58 100644 --- a/app/views/settings/migrations/show.html.haml +++ b/app/views/settings/migrations/show.html.haml @@ -48,9 +48,9 @@ .fields-row__column.fields-group.fields-row__column-6 - if current_user.encrypted_password.present? - = f.input :current_password, wrapper: :with_block_label, input_html: { :autocomplete => 'current-password' }, required: true, disabled: on_cooldown? + = f.input :current_password, wrapper: :with_block_label, input_html: { autocomplete: 'current-password' }, required: true, disabled: on_cooldown? - else - = f.input :current_username, wrapper: :with_block_label, input_html: { :autocomplete => 'off' }, required: true, disabled: on_cooldown? + = f.input :current_username, wrapper: :with_block_label, input_html: { autocomplete: 'off' }, required: true, disabled: on_cooldown? .actions = f.button :button, t('migrations.proceed_with_move'), type: :submit, class: 'button button--destructive', disabled: on_cooldown? diff --git a/app/views/settings/two_factor_authentication/confirmations/new.html.haml b/app/views/settings/two_factor_authentication/confirmations/new.html.haml index 671237db5..43830ac27 100644 --- a/app/views/settings/two_factor_authentication/confirmations/new.html.haml +++ b/app/views/settings/two_factor_authentication/confirmations/new.html.haml @@ -12,7 +12,7 @@ %samp.qr-alternative__code= @new_otp_secret.scan(/.{4}/).join(' ') .fields-group - = f.input :otp_attempt, wrapper: :with_label, hint: t('otp_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { :autocomplete => 'off' }, required: true + = f.input :otp_attempt, wrapper: :with_label, hint: t('otp_authentication.code_hint'), label: t('simple_form.labels.defaults.otp_attempt'), input_html: { autocomplete: 'off' }, required: true .actions = f.button :button, t('otp_authentication.enable'), type: :submit diff --git a/app/views/settings/two_factor_authentication/webauthn_credentials/new.html.haml b/app/views/settings/two_factor_authentication/webauthn_credentials/new.html.haml index 1148d5ed7..5e9d22571 100644 --- a/app/views/settings/two_factor_authentication/webauthn_credentials/new.html.haml +++ b/app/views/settings/two_factor_authentication/webauthn_credentials/new.html.haml @@ -8,7 +8,7 @@ %p.hint= t('webauthn_credentials.description_html') .fields_group - = f.input :nickname, wrapper: :with_block_label, hint: t('webauthn_credentials.nickname_hint'), input_html: { :autocomplete => 'off' }, required: true + = f.input :nickname, wrapper: :with_block_label, hint: t('webauthn_credentials.nickname_hint'), input_html: { autocomplete: 'off' }, required: true .actions = f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 619406d89..bf498e33d 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -15,12 +15,12 @@ = account_action_button(status.account) - .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< + .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? %p< %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}  %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ :lang => status.language } + .e-content{ lang: status.language } = prerender_custom_emojis(status_content_format(status), status.emojis) - if status.preloadable_poll diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index bfde3a260..32584c92a 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -27,12 +27,12 @@ %span.display-name__account = acct(status.account) = fa_icon('lock') if status.account.locked? - .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< + .status__content.emojify{ data: ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? %p< %span.p-summary> #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}  %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ :lang => status.language } + .e-content{ lang: status.language } = prerender_custom_emojis(status_content_format(status), status.emojis) - if status.preloadable_poll diff --git a/config/environments/production.rb b/config/environments/production.rb index dc5319535..5ea9ea9ba 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -116,18 +116,18 @@ Rails.application.configure do end config.action_mailer.smtp_settings = { - :port => ENV['SMTP_PORT'], - :address => ENV['SMTP_SERVER'], - :user_name => ENV['SMTP_LOGIN'].presence, - :password => ENV['SMTP_PASSWORD'].presence, - :domain => ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'], - :authentication => ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain, - :ca_file => ENV['SMTP_CA_FILE'].presence || '/etc/ssl/certs/ca-certificates.crt', - :openssl_verify_mode => ENV['SMTP_OPENSSL_VERIFY_MODE'], - :enable_starttls => enable_starttls, - :enable_starttls_auto => enable_starttls_auto, - :tls => ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', - :ssl => ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', + port: ENV['SMTP_PORT'], + address: ENV['SMTP_SERVER'], + user_name: ENV['SMTP_LOGIN'].presence, + password: ENV['SMTP_PASSWORD'].presence, + domain: ENV['SMTP_DOMAIN'] || ENV['LOCAL_DOMAIN'], + authentication: ENV['SMTP_AUTH_METHOD'] == 'none' ? nil : ENV['SMTP_AUTH_METHOD'] || :plain, + ca_file: ENV['SMTP_CA_FILE'].presence || '/etc/ssl/certs/ca-certificates.crt', + openssl_verify_mode: ENV['SMTP_OPENSSL_VERIFY_MODE'], + enable_starttls: enable_starttls, + enable_starttls_auto: enable_starttls_auto, + tls: ENV['SMTP_TLS'].presence && ENV['SMTP_TLS'] == 'true', + ssl: ENV['SMTP_SSL'].presence && ENV['SMTP_SSL'] == 'true', } config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb index 227688e23..7f912c1c0 100644 --- a/spec/controllers/admin/statuses_controller_spec.rb +++ b/spec/controllers/admin/statuses_controller_spec.rb @@ -41,7 +41,7 @@ describe Admin::StatusesController do describe 'POST #batch' do before do - post :batch, params: { :account_id => account.id, action => '', :admin_status_batch_action => { status_ids: status_ids } } + post :batch, params: { account_id: account.id, action => '', admin_status_batch_action: { status_ids: status_ids } } end let(:status_ids) { [media_attached_status.id] } diff --git a/spec/services/favourite_service_spec.rb b/spec/services/favourite_service_spec.rb index 94a8111dd..9781f0d78 100644 --- a/spec/services/favourite_service_spec.rb +++ b/spec/services/favourite_service_spec.rb @@ -23,7 +23,7 @@ RSpec.describe FavouriteService, type: :service do let(:status) { Fabricate(:status, account: bob) } before do - stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) + stub_request(:post, "http://example.com/inbox").to_return(status: 200, body: "", headers: {}) subject.call(sender, status) end diff --git a/spec/services/follow_service_spec.rb b/spec/services/follow_service_spec.rb index 88346ec54..412c04d76 100644 --- a/spec/services/follow_service_spec.rb +++ b/spec/services/follow_service_spec.rb @@ -140,7 +140,7 @@ RSpec.describe FollowService, type: :service do let(:bob) { Fabricate(:account, username: 'bob', domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } before do - stub_request(:post, "http://example.com/inbox").to_return(:status => 200, :body => "", :headers => {}) + stub_request(:post, "http://example.com/inbox").to_return(status: 200, body: "", headers: {}) subject.call(sender, bob) end diff --git a/spec/views/statuses/show.html.haml_spec.rb b/spec/views/statuses/show.html.haml_spec.rb index eeea2f698..ca5bb2ae8 100644 --- a/spec/views/statuses/show.html.haml_spec.rb +++ b/spec/views/statuses/show.html.haml_spec.rb @@ -4,7 +4,7 @@ require 'rails_helper' describe 'statuses/show.html.haml', without_verify_partial_doubles: true do before do - double(:api_oembed_url => '') + double(api_oembed_url: '') allow(view).to receive(:show_landing_strip?).and_return(true) allow(view).to receive(:site_title).and_return('example site') allow(view).to receive(:site_hostname).and_return('example.com') From 585cc1a604f6c445436b5bea23c1eb2f899300c3 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 17 Nov 2022 11:24:59 +0100 Subject: [PATCH 20/20] Remove use of DOMParser in front-end emoji rewriting code (#20758) * Add jstest for node ordering in emojify * Remove use of DOMParser in front-end emoji rewriting code --- .../features/emoji/__tests__/emoji-test.js | 5 ++++ .../mastodon/features/emoji/emoji.js | 23 +++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 2f19aab7e..72a732e3b 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -88,5 +88,10 @@ describe('emoji', () => { expect(emojify('šŸ’‚ā€ā™€ļøšŸ’‚ā€ā™‚ļø')) .toEqual('šŸ’‚\u200Dā™€ļøšŸ’‚\u200Dā™‚ļø'); }); + + it('keeps ordering as expected (issue fixed by PR 20677)', () => { + expect(emojify('

šŸ’• #foo test: foo.

')) + .toEqual('

šŸ’• #foo test: foo.

'); + }); }); }); diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 52a8458fb..bc3dd8c60 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -19,8 +19,6 @@ const emojiFilename = (filename) => { return borderedEmoji.includes(filename) ? (filename + '_border') : filename; }; -const domParser = new DOMParser(); - const emojifyTextNode = (node, customEmojis) => { let str = node.textContent; @@ -39,7 +37,7 @@ const emojifyTextNode = (node, customEmojis) => { } } - let rend, replacement = ''; + let rend, replacement = null; if (i === str.length) { break; } else if (str[i] === ':') { @@ -51,7 +49,14 @@ const emojifyTextNode = (node, customEmojis) => { // if you want additional emoji handler, add statements below which set replacement and return true. if (shortname in customEmojis) { const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url; - replacement = `${shortname}`; + replacement = document.createElement('img'); + replacement.setAttribute('draggable', false); + replacement.setAttribute('class', 'emojione custom-emoji'); + replacement.setAttribute('alt', shortname); + replacement.setAttribute('title', shortname); + replacement.setAttribute('src', filename); + replacement.setAttribute('data-original', customEmojis[shortname].url); + replacement.setAttribute('data-static', customEmojis[shortname].static_url); return true; } return false; @@ -59,7 +64,12 @@ const emojifyTextNode = (node, customEmojis) => { } else { // matched to unicode emoji const { filename, shortCode } = unicodeMapping[match]; const title = shortCode ? `:${shortCode}:` : ''; - replacement = `${match}`; + replacement = document.createElement('img'); + replacement.setAttribute('draggable', false); + replacement.setAttribute('class', 'emojione'); + replacement.setAttribute('alt', match); + replacement.setAttribute('title', title); + replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`); rend = i + match.length; // If the matched character was followed by VS15 (for selecting text presentation), skip it. if (str.codePointAt(rend) === 65038) { @@ -69,9 +79,8 @@ const emojifyTextNode = (node, customEmojis) => { fragment.append(document.createTextNode(str.slice(0, i))); if (replacement) { - fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]); + fragment.append(replacement); } - node.textContent = str.slice(0, i); str = str.slice(rend); }