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 7080095f2d..7d5bd3c919 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 552def142d..286170c9ff 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 84b649f5c1..43aac5769f 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 5604325be4..48f3aa7a6a 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 5cfcf7205b..473622edf4 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 7d5bd3c919..8d67e55ebe 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 4e109080ee..bbb0ca1c62 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 f7ea661d7d..2b99ac502b 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 f0dfd044cc..eae2bdc010 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 679e356b41..048c0f6824 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 6b62e6f337..cb56293376 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 be234cdcb9..e912967fd7 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 6d566b1c26..3d0298927a 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 e901fb9323..30817d0252 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 1a0ddfa877..0d167ea3e7 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 32f71bdce2..0966ee4699 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 5ceea5d3c1..56229fd05c 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 0a39d7f26e..7496fe2d51 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 3fc88e60e4..52ba454cce 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 39fe1bd0b7..6c12bd0730 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 cf311fef23..57274cfd98 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 048c0f6824..3a80c125d3 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 16defc1ea8..e79f7a43e1 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 593457b94e..a0a43de192 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 0000000000..57fb12c620 --- /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 0000000000..fb0cd05d29 --- /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 0000000000..b40c76557f --- /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 de86e0e117..4e817129d6 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 0000000000..79c0722d53 --- /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 65f494fed4..9e746b9157 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 ad1dc2a38e..8e298ac9d7 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 0000000000..39012df517 --- /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 0000000000..dc0cf8c52a --- /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 0000000000..5d4b6c4d0d --- /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 0000000000..01add232d1 --- /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 0000000000..0291aeed77 --- /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 cc50203986..8f7e3e67d2 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 3a80c125d3..1cc53dca4b 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 98aa5a0330..98e19667cf 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 0000000000..6c4e677876 --- /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 5c2dcd2687..98cda50047 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 0000000000..1e1a5ae7d4 --- /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 0000000000..8697e0c215 --- /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 0000000000..4200ac3f52 --- /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 0000000000..28ffb91751 --- /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 8dc2d1c479..38a413c2e3 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 909d9ff818..3d0e6b1dad 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 ff4b7a506f..4f21e4af6e 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 4a1bedaa42..a294d3cb5f 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 c7dbebe756..b95a9b676b 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 bae5b24ba8..10ad108eaf 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 5d993f574a..c094dfd255 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 c642c2293b..60fd1635ef 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 b1d52dd0c2..0d8fd800f9 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 943618e390..304e3ab849 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 82f9575275..094b502b17 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 c14fed56f8..1a6611ceb6 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 c08ee85b0b..2e9785c898 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 d7868e900d..3700878799 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 1ecf7302a9..31f7d5e58d 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 671237db57..43830ac27f 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 1148d5ed7e..5e9d225718 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 619406d897..bf498e33d5 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 bfde3a2608..32584c92a1 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 dc53195359..5ea9ea9bac 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 227688e236..7f912c1c07 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 94a8111dd5..9781f0d78b 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 88346ec54a..412c04d76b 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 eeea2f6985..ca5bb2ae87 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 2f19aab7e4..72a732e3bc 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 52a8458fbb..bc3dd8c602 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); }