From df66c290c0f1516800291ced31879baaa4760e03 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 c18c565cb30243b34fb28fa79f1f4e039518fb5f 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 2e118aa0e6e109d5ea958a7ef33091b1f0468a7c 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 5573d1db4ecdeadeae06db3a1c6cd8d5d87a10e1 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 3e40b7ef8d36c3ed671a2d75806001e6b1f5036a 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 7619476cf14f0cad39a762daa9436c59cb1ade67 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 8f32b5957086c8eafa14c421e4042bd932a5e661 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 9b83c67aa77f1b26283c49d1edd92f35d798b33b 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 d79515d3df97d96d21b86e92b7de9ef62c45a647 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 d5fad31a45443b539ad2506af8528b0ee5f22102 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 c7accdb6ac04a99044cc01e1f6659a975004da96 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 0f402ff0ebeed5414d938db1c68f24ad0f0abbc7 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 699bec589f4c5b91951169867dd708bffdd7d5b3 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 4abfb043f27d11bdd095769e13b680a6269f787a 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 e0940d04f1731d38000863cfb10a159965fcabd2 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 5a5fd023d1458cab5965e668acb73aa260072de1 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 739d6844037c8ed1abfb4a546a01592607ae2ceb 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 498e9478c7c1434a26f9f0f30cd86d6d96f0e9a3 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 b8319d057824bc4bcdbac2cad16f97e264844168) * 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 73a2e3326fe25d52d7067d946be509aeb0eea5c2) * Fix authorization check in domain blocks controller (cherry picked from commit 655bac2c3aed67eed868d32d1a5b02df4c46e6b4) * 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 1ba41b34c1dfc872f0f19c146a38b0b21e4ee3f7) * Fix domain block export not exporting blocks with only media rejection (cherry picked from commit 1686374b2f2a380de63b616be56be7db918b35f0) * 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 b54c6394825200d0d84bc1febee975be212ffc9f 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 41be06e4ec8ce28a1af66bd18e9527cba47ff278 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); }