Merge tag 'v4.0.4' into hometown-4.0.4
This commit is contained in:
commit
9fe562c31c
|
@ -68,7 +68,9 @@ jobs:
|
|||
cache-version: v1
|
||||
pkg-manager: yarn
|
||||
- run:
|
||||
command: ./bin/rails assets:precompile
|
||||
command: |
|
||||
export NODE_OPTIONS=--openssl-legacy-provider
|
||||
./bin/rails assets:precompile
|
||||
name: Precompile assets
|
||||
- persist_to_workspace:
|
||||
paths:
|
||||
|
|
|
@ -12,6 +12,7 @@ on:
|
|||
- Dockerfile
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
|
@ -20,15 +21,28 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- uses: docker/setup-qemu-action@v2
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/login-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
|
||||
|
||||
- name: Log in to the Github Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
if: github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request'
|
||||
|
||||
- uses: docker/metadata-action@v4
|
||||
id: meta
|
||||
with:
|
||||
images: tootsuite/mastodon
|
||||
images: |
|
||||
tootsuite/mastodon
|
||||
ghcr.io/mastodon/mastodon
|
||||
flavor: |
|
||||
latest=auto
|
||||
tags: |
|
||||
|
@ -36,11 +50,15 @@ jobs:
|
|||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
- uses: docker/build-push-action@v3
|
||||
|
||||
- uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
provenance: false
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.repository == 'mastodon/mastodon' && github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=registry,ref=tootsuite/mastodon:edge
|
||||
cache-to: type=inline
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
|
|
@ -1 +1 @@
|
|||
3.0.4
|
||||
3.0.6
|
||||
|
|
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -3,6 +3,53 @@ Changelog
|
|||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.0.4] - 2023-04-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix crash in `tootctl` commands making use of parallelization when Elasticsearch is enabled ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24182), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/24377))
|
||||
- Fix crash in `db:setup` when Elasticsearch is enabled ([rrgeorge](https://github.com/mastodon/mastodon/pull/24302))
|
||||
- Fix user archive takeout when using OpenStack Swift or S3 providers with no ACL support ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24200))
|
||||
- Fix invalid/expired invites being processed on sign-up ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24337))
|
||||
|
||||
### Security
|
||||
|
||||
- Update Ruby to 3.0.6 due to ReDoS vulnerabilities ([saizai](https://github.com/mastodon/mastodon/pull/24333))
|
||||
- Fix unescaped user input in LDAP query ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24379))
|
||||
|
||||
# [4.0.3] - 2023-03-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add redirection from paths with url-encoded `@` to their decoded form ([thijskh](https://github.com/mastodon/mastodon/pull/23593))
|
||||
- Add `lang` attribute to native language names in language picker in Web UI ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23749))
|
||||
- Add headers to outgoing mails to avoid auto-replies ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23597))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix “Remove all followers from the selected domains” being more destructive than it claims ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23805))
|
||||
- Fix case-sensitive check for previously used hashtags in hashtag autocompletion ([deanveloper](https://github.com/mastodon/mastodon/pull/23526))
|
||||
- Fix sidebar behavior in settings/admin UI on mobile ([wxt2005](https://github.com/mastodon/mastodon/pull/23764))
|
||||
- Fix inefficiency when searching accounts per username in admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23801))
|
||||
- Fix server error when failing to follow back followers from `/relationships` ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23787))
|
||||
- Fix server error when attempting to display the edit history of a trendable post in the admin interface ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23574))
|
||||
- Fix original account being unfollowed on migration before the follow request to the new account could be sent ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/21957))
|
||||
- Fix pgBouncer resetting application name on every transaction ([Gargron](https://github.com/mastodon/mastodon/pull/23958))
|
||||
- Fix unconfirmed accounts being counted as active users ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23803))
|
||||
- Fix `/api/v1/streaming` sub-paths not being redirected ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23988))
|
||||
- Fix drag'n'drop upload area text that spans multiple lines not being centered ([vintprox](https://github.com/mastodon/mastodon/pull/24029))
|
||||
- Fix sidekiq jobs not triggering Elasticsearch index updates ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24046))
|
||||
- Fix tags being unnecessarily stripped from plain-text short site description ([c960657](https://github.com/mastodon/mastodon/pull/23975))
|
||||
- Fix HTML entities not being un-escaped in extracted plain-text from remote posts ([c960657](https://github.com/mastodon/mastodon/pull/24019))
|
||||
- Fix dashboard crash on ElasticSearch server error ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23751))
|
||||
- Fix incorrect post links in strikes when the account is remote ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23611))
|
||||
- Fix misleading error code when receiving invalid WebAuthn credentials ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/23568))
|
||||
|
||||
### Security
|
||||
|
||||
- Change user backups to use expiring URLs for download when possible ([Gargron](https://github.com/mastodon/mastodon/pull/24136))
|
||||
- Add warning for object storage misconfiguration ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/24137))
|
||||
|
||||
## [4.0.2] - 2022-11-15
|
||||
### Fixed
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ RUN ARCH= && \
|
|||
mv node-v$NODE_VER-linux-$ARCH /opt/node
|
||||
|
||||
# Install Ruby 3.0
|
||||
ENV RUBY_VER="3.0.4"
|
||||
ENV RUBY_VER="3.0.6"
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends build-essential \
|
||||
bison libyaml-dev libgdbm-dev libreadline-dev libjemalloc-dev \
|
||||
|
|
|
@ -48,7 +48,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
|
|||
super(hash)
|
||||
|
||||
resource.locale = I18n.locale
|
||||
resource.invite_code = params[:invite_code] if resource.invite_code.blank?
|
||||
resource.invite_code = @invite&.code if resource.invite_code.blank?
|
||||
resource.registration_form_time = session[:registration_form_time]
|
||||
resource.sign_up_ip = request.remote_ip
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BackupsController < ApplicationController
|
||||
include RoutingHelper
|
||||
|
||||
skip_before_action :require_functional!
|
||||
|
||||
before_action :authenticate_user!
|
||||
before_action :set_backup
|
||||
|
||||
def download
|
||||
case Paperclip::Attachment.default_options[:storage]
|
||||
when :s3
|
||||
redirect_to @backup.dump.expiring_url(10)
|
||||
when :fog
|
||||
if Paperclip::Attachment.default_options.dig(:storage, :fog_credentials, :openstack_temp_url_key).present?
|
||||
redirect_to @backup.dump.expiring_url(Time.now.utc + 10)
|
||||
else
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
end
|
||||
when :filesystem
|
||||
redirect_to full_asset_url(@backup.dump.url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_backup
|
||||
@backup = current_user.backups.find(params[:id])
|
||||
end
|
||||
end
|
|
@ -19,6 +19,8 @@ class RelationshipsController < ApplicationController
|
|||
@form.save
|
||||
rescue ActionController::ParameterMissing
|
||||
# Do nothing
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound
|
||||
flash[:alert] = I18n.t('relationships.follow_failure') if action_from_button == 'follow'
|
||||
ensure
|
||||
redirect_to relationships_path(filter_params)
|
||||
end
|
||||
|
@ -60,8 +62,8 @@ class RelationshipsController < ApplicationController
|
|||
'unfollow'
|
||||
elsif params[:remove_from_followers]
|
||||
'remove_from_followers'
|
||||
elsif params[:block_domains]
|
||||
'block_domains'
|
||||
elsif params[:block_domains] || params[:remove_domains_from_followers]
|
||||
'remove_domains_from_followers'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ module Settings
|
|||
end
|
||||
else
|
||||
flash[:error] = I18n.t('webauthn_credentials.create.error')
|
||||
status = :internal_server_error
|
||||
status = :unprocessable_entity
|
||||
end
|
||||
else
|
||||
flash[:error] = t('webauthn_credentials.create.error')
|
||||
|
|
|
@ -216,7 +216,7 @@ class LanguageDropdownMenu extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<div key={lang[0]} role='option' tabIndex='0' data-index={lang[0]} className={classNames('language-dropdown__dropdown__results__item', { active: lang[0] === value })} aria-selected={lang[0] === value} onClick={this.handleClick} onKeyDown={this.handleKeyDown}>
|
||||
<span className='language-dropdown__dropdown__results__item__native-name'>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
|
||||
<span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -190,11 +190,12 @@ const ignoreSuggestion = (state, position, token, completion, path) => {
|
|||
};
|
||||
|
||||
const sortHashtagsByUse = (state, tags) => {
|
||||
const personalHistory = state.get('tagHistory');
|
||||
const personalHistory = state.get('tagHistory').map(tag => tag.toLowerCase());
|
||||
|
||||
return tags.sort((a, b) => {
|
||||
const usedA = personalHistory.includes(a.name);
|
||||
const usedB = personalHistory.includes(b.name);
|
||||
const tagsWithLowercase = tags.map(t => ({ ...t, lowerName: t.name.toLowerCase() }));
|
||||
const sorted = tagsWithLowercase.sort((a, b) => {
|
||||
const usedA = personalHistory.includes(a.lowerName);
|
||||
const usedB = personalHistory.includes(b.lowerName);
|
||||
|
||||
if (usedA === usedB) {
|
||||
return 0;
|
||||
|
@ -204,6 +205,8 @@ const sortHashtagsByUse = (state, tags) => {
|
|||
return 1;
|
||||
}
|
||||
});
|
||||
sorted.forEach(tag => delete tag.lowerName);
|
||||
return sorted;
|
||||
};
|
||||
|
||||
const insertEmoji = (state, position, emojiData, needsSpace) => {
|
||||
|
|
|
@ -400,7 +400,7 @@ $content-width: 840px;
|
|||
position: fixed;
|
||||
z-index: 10;
|
||||
width: 100%;
|
||||
height: calc(100vh - 56px);
|
||||
height: calc(100% - 56px);
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
|
|
|
@ -4498,6 +4498,7 @@ a.status-card.compact:hover {
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
color: $secondary-text-color;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Admin::SystemCheck
|
||||
ACTIVE_CHECKS = [
|
||||
Admin::SystemCheck::MediaPrivacyCheck,
|
||||
Admin::SystemCheck::DatabaseSchemaCheck,
|
||||
Admin::SystemCheck::SidekiqProcessCheck,
|
||||
Admin::SystemCheck::RulesCheck,
|
||||
|
|
|
@ -24,7 +24,7 @@ class Admin::SystemCheck::ElasticsearchCheck < Admin::SystemCheck::BaseCheck
|
|||
def running_version
|
||||
@running_version ||= begin
|
||||
Chewy.client.info['version']['number']
|
||||
rescue Faraday::ConnectionFailed
|
||||
rescue Faraday::ConnectionFailed, Elasticsearch::Transport::Transport::Error
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SystemCheck::MediaPrivacyCheck < Admin::SystemCheck::BaseCheck
|
||||
include RoutingHelper
|
||||
|
||||
def skip?
|
||||
!current_user.can?(:view_devops)
|
||||
end
|
||||
|
||||
def pass?
|
||||
check_media_uploads!
|
||||
@failure_message.nil?
|
||||
end
|
||||
|
||||
def message
|
||||
Admin::SystemCheck::Message.new(@failure_message, @failure_value, @failure_action, true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def check_media_uploads!
|
||||
if Rails.configuration.x.use_s3
|
||||
check_media_listing_inaccessible_s3!
|
||||
else
|
||||
check_media_listing_inaccessible!
|
||||
end
|
||||
end
|
||||
|
||||
def check_media_listing_inaccessible!
|
||||
full_url = full_asset_url(media_attachment.file.url(:original, false))
|
||||
|
||||
# Check if we can list the uploaded file. If true, that's an error
|
||||
directory_url = Addressable::URI.parse(full_url)
|
||||
directory_url.query = nil
|
||||
filename = directory_url.path.gsub(%r{.*/}, '')
|
||||
directory_url.path = directory_url.path.gsub(%r{/[^/]+\Z}, '/')
|
||||
Request.new(:get, directory_url, allow_local: true).perform do |res|
|
||||
if res.truncated_body&.include?(filename)
|
||||
@failure_message = use_storage? ? :upload_check_privacy_error_object_storage : :upload_check_privacy_error
|
||||
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#FS'
|
||||
end
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def check_media_listing_inaccessible_s3!
|
||||
urls_to_check = []
|
||||
paperclip_options = Paperclip::Attachment.default_options
|
||||
s3_protocol = paperclip_options[:s3_protocol]
|
||||
s3_host_alias = paperclip_options[:s3_host_alias]
|
||||
s3_host_name = paperclip_options[:s3_host_name]
|
||||
bucket_name = paperclip_options.dig(:s3_credentials, :bucket)
|
||||
|
||||
urls_to_check << "#{s3_protocol}://#{s3_host_alias}/" if s3_host_alias.present?
|
||||
urls_to_check << "#{s3_protocol}://#{s3_host_name}/#{bucket_name}/"
|
||||
urls_to_check.uniq.each do |full_url|
|
||||
check_s3_listing!(full_url)
|
||||
break if @failure_message.present?
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def check_s3_listing!(full_url)
|
||||
bucket_url = Addressable::URI.parse(full_url)
|
||||
bucket_url.path = bucket_url.path.delete_suffix(media_attachment.file.path(:original))
|
||||
bucket_url.query = "max-keys=1&x-random=#{SecureRandom.hex(10)}"
|
||||
Request.new(:get, bucket_url, allow_local: true).perform do |res|
|
||||
if res.truncated_body&.include?('ListBucketResult')
|
||||
@failure_message = :upload_check_privacy_error_object_storage
|
||||
@failure_action = 'https://docs.joinmastodon.org/admin/optional/object-storage/#S3'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def media_attachment
|
||||
@media_attachment ||= begin
|
||||
attachment = Account.representative.media_attachments.first
|
||||
if attachment.present?
|
||||
attachment.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
attachment
|
||||
else
|
||||
create_test_attachment!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def create_test_attachment!
|
||||
Tempfile.create(%w(test-upload .jpg), binmode: true) do |tmp_file|
|
||||
tmp_file.write(
|
||||
Base64.decode64(
|
||||
'/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' \
|
||||
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' \
|
||||
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' \
|
||||
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' \
|
||||
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' \
|
||||
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
|
||||
)
|
||||
)
|
||||
tmp_file.flush
|
||||
Account.representative.media_attachments.create!(file: tmp_file)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SystemCheck::Message
|
||||
attr_reader :key, :value, :action
|
||||
attr_reader :key, :value, :action, :critical
|
||||
|
||||
def initialize(key, value = nil, action = nil)
|
||||
@key = key
|
||||
@value = value
|
||||
@action = action
|
||||
def initialize(key, value = nil, action = nil, critical = false)
|
||||
@key = key
|
||||
@value = value
|
||||
@action = action
|
||||
@critical = critical
|
||||
end
|
||||
end
|
||||
|
|
|
@ -18,7 +18,7 @@ class PlainTextFormatter
|
|||
if local?
|
||||
text
|
||||
else
|
||||
strip_tags(insert_newlines).chomp
|
||||
html_entities.decode(strip_tags(insert_newlines)).chomp
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -27,4 +27,8 @@ class PlainTextFormatter
|
|||
def insert_newlines
|
||||
text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
|
||||
end
|
||||
|
||||
def html_entities
|
||||
HTMLEntities.new
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,8 @@ class ApplicationMailer < ActionMailer::Base
|
|||
helper :instance
|
||||
helper :formatting
|
||||
|
||||
after_action :set_autoreply_headers!
|
||||
|
||||
protected
|
||||
|
||||
def locale_for_account(account)
|
||||
|
@ -14,4 +16,10 @@ class ApplicationMailer < ActionMailer::Base
|
|||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def set_autoreply_headers!
|
||||
headers['Precedence'] = 'list'
|
||||
headers['X-Auto-Response-Suppress'] = 'All'
|
||||
headers['Auto-Submitted'] = 'auto-generated'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -107,7 +107,7 @@ class Account < ApplicationRecord
|
|||
scope :bots, -> { where(actor_type: %w(Application Service)) }
|
||||
scope :groups, -> { where(actor_type: 'Group') }
|
||||
scope :alphabetic, -> { order(domain: :asc, username: :asc) }
|
||||
scope :matches_username, ->(value) { where(arel_table[:username].matches("#{value}%")) }
|
||||
scope :matches_username, ->(value) { where('lower((username)::text) LIKE lower(?)', "#{value}%") }
|
||||
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
|
||||
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
|
||||
scope :without_unapproved, -> { left_outer_joins(:user).remote.or(left_outer_joins(:user).merge(User.approved.confirmed)) }
|
||||
|
|
|
@ -17,6 +17,6 @@
|
|||
class Backup < ApplicationRecord
|
||||
belongs_to :user, inverse_of: :backups
|
||||
|
||||
has_attached_file :dump
|
||||
has_attached_file :dump, s3_permissions: ->(*) { ENV['S3_PERMISSION'] == '' ? nil : 'private' }
|
||||
do_not_validate_attachment_file_type :dump
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ module LdapAuthenticable
|
|||
class_methods do
|
||||
def authenticate_with_ldap(params = {})
|
||||
ldap = Net::LDAP.new(ldap_options)
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: params[:email])
|
||||
filter = format(Devise.ldap_search_filter, uid: Devise.ldap_uid, mail: Devise.ldap_mail, email: Net::LDAP::Filter.escape(params[:email]))
|
||||
|
||||
if (user_info = ldap.bind_as(base: Devise.ldap_base, filter: filter, password: params[:password]))
|
||||
ldap_get_user(user_info.first)
|
||||
|
|
|
@ -17,8 +17,8 @@ class Form::AccountBatch
|
|||
unfollow!
|
||||
when 'remove_from_followers'
|
||||
remove_from_followers!
|
||||
when 'block_domains'
|
||||
block_domains!
|
||||
when 'remove_domains_from_followers'
|
||||
remove_domains_from_followers!
|
||||
when 'approve'
|
||||
approve!
|
||||
when 'reject'
|
||||
|
@ -35,9 +35,15 @@ class Form::AccountBatch
|
|||
private
|
||||
|
||||
def follow!
|
||||
error = nil
|
||||
|
||||
accounts.each do |target_account|
|
||||
FollowService.new.call(current_account, target_account)
|
||||
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound => e
|
||||
error ||= e
|
||||
end
|
||||
|
||||
raise error if error.present?
|
||||
end
|
||||
|
||||
def unfollow!
|
||||
|
@ -50,10 +56,8 @@ class Form::AccountBatch
|
|||
RemoveFromFollowersService.new.call(current_account, account_ids)
|
||||
end
|
||||
|
||||
def block_domains!
|
||||
AfterAccountDomainBlockWorker.push_bulk(account_domains) do |domain|
|
||||
[current_account.id, domain]
|
||||
end
|
||||
def remove_domains_from_followers!
|
||||
RemoveDomainsFromFollowersService.new.call(current_account, account_domains)
|
||||
end
|
||||
|
||||
def account_domains
|
||||
|
|
|
@ -480,10 +480,13 @@ class User < ApplicationRecord
|
|||
def prepare_new_user!
|
||||
BootstrapTimelineWorker.perform_async(account_id)
|
||||
ActivityTracker.increment('activity:accounts:local')
|
||||
ActivityTracker.record('activity:logins', id)
|
||||
UserMailer.welcome(self).deliver_later
|
||||
end
|
||||
|
||||
def prepare_returning_user!
|
||||
return unless confirmed?
|
||||
|
||||
ActivityTracker.record('activity:logins', id)
|
||||
regenerate_feed! if needs_feed_update?
|
||||
end
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FollowMigrationService < FollowService
|
||||
# Follow an account with the same settings as another account, and unfollow the old account once the request is sent
|
||||
# @param [Account] source_account From which to follow
|
||||
# @param [Account] target_account Account to follow
|
||||
# @param [Account] old_target_account Account to unfollow once the follow request has been sent to the new one
|
||||
# @option [Boolean] bypass_locked Whether to immediately follow the new account even if it is locked
|
||||
def call(source_account, target_account, old_target_account, bypass_locked: false)
|
||||
@old_target_account = old_target_account
|
||||
|
||||
follow = source_account.active_relationships.find_by(target_account: old_target_account)
|
||||
reblogs = follow&.show_reblogs?
|
||||
notify = follow&.notify?
|
||||
languages = follow&.languages
|
||||
|
||||
super(source_account, target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_follow!
|
||||
follow_request = @source_account.request_follow!(@target_account, **follow_options.merge(rate_limit: @options[:with_rate_limit], bypass_limit: @options[:bypass_limit]))
|
||||
|
||||
if @target_account.local?
|
||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
|
||||
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
|
||||
elsif @target_account.activitypub?
|
||||
ActivityPub::MigratedFollowDeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, @old_target_account.id)
|
||||
end
|
||||
|
||||
follow_request
|
||||
end
|
||||
|
||||
def direct_follow!
|
||||
follow = super
|
||||
UnfollowService.new.call(@source_account, @old_target_account, skip_unmerge: true)
|
||||
follow
|
||||
end
|
||||
end
|
|
@ -0,0 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveDomainsFromFollowersService < BaseService
|
||||
include Payloadable
|
||||
|
||||
def call(source_account, target_domains)
|
||||
source_account.passive_relationships.where(account_id: Account.where(domain: target_domains)).find_each do |follow|
|
||||
follow.destroy
|
||||
|
||||
create_notification(follow) if source_account.local? && !follow.account.local? && follow.account.activitypub?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_notification(follow)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.target_account_id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
def build_json(follow)
|
||||
Oj.dump(serialize_payload(follow, ActivityPub::RejectFollowSerializer))
|
||||
end
|
||||
end
|
|
@ -12,7 +12,7 @@
|
|||
- unless @system_checks.empty?
|
||||
.flash-message-stack
|
||||
- @system_checks.each do |message|
|
||||
.flash-message.warning
|
||||
.flash-message{ class: message.critical ? 'alert' : 'warning' }
|
||||
= t("admin.system_checks.#{message.key}.message_html", value: message.value ? content_tag(:strong, message.value) : nil)
|
||||
- if message.action
|
||||
= link_to t("admin.system_checks.#{message.key}.action"), message.action
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
%td
|
||||
- if @status.trend.allowed?
|
||||
%abbr{ title: t('admin.trends.tags.current_score', score: @status.trend.score) }= t('admin.trends.tags.trending_rank', rank: @status.trend.rank)
|
||||
- elsif @status.trend.requires_review?
|
||||
- elsif @status.requires_review?
|
||||
= t('admin.trends.pending_review')
|
||||
- else
|
||||
= t('admin.trends.not_allowed_to_trend')
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
= image_tag @instance_presenter.thumbnail&.file&.url(:'@1x') || asset_pack_path('media/images/preview.png'), alt: @instance_presenter.title
|
||||
|
||||
.hero-widget__text
|
||||
%p= @instance_presenter.description.html_safe.presence || t('about.about_mastodon_html')
|
||||
%p= @instance_presenter.description.presence || t('about.about_mastodon_html')
|
||||
|
||||
- if Setting.trends && !(user_signed_in? && !current_user.setting_trends)
|
||||
- trends = Trends.tags.query.allowed.limit(3)
|
||||
|
|
|
@ -50,15 +50,15 @@
|
|||
.strike-card__statuses-list__item
|
||||
- if (status = status_map[status_id.to_i])
|
||||
.one-liner
|
||||
= link_to short_account_status_url(@strike.target_account, status_id), class: 'emojify' do
|
||||
= one_line_preview(status)
|
||||
.emojify= one_line_preview(status)
|
||||
|
||||
- status.ordered_media_attachments.each do |media_attachment|
|
||||
%abbr{ title: media_attachment.description }
|
||||
= fa_icon 'link'
|
||||
= media_attachment.file_file_name
|
||||
- status.ordered_media_attachments.each do |media_attachment|
|
||||
%abbr{ title: media_attachment.description }
|
||||
= fa_icon 'link'
|
||||
= media_attachment.file_file_name
|
||||
.strike-card__statuses-list__item__meta
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status), target: '_blank' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
- unless status.application.nil?
|
||||
·
|
||||
= status.application.name
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_followers')]), name: :remove_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } unless following_relationship?
|
||||
|
||||
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :block_domains, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
|
||||
= f.button safe_join([fa_icon('trash'), t('relationships.remove_selected_domains')]), name: :remove_domains_from_followers, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') } if followed_by_relationship?
|
||||
.batch-table__body
|
||||
- if @accounts.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
|
|
|
@ -64,6 +64,6 @@
|
|||
%td= l backup.created_at
|
||||
- if backup.processed?
|
||||
%td= number_to_human_size backup.dump_file_size
|
||||
%td= table_link_to 'download', t('exports.archive_takeout.download'), backup.dump.url
|
||||
%td= table_link_to 'download', t('exports.archive_takeout.download'), download_backup_url(backup)
|
||||
- else
|
||||
%td{ colspan: 2 }= t('exports.archive_takeout.in_progress')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- thumbnail = @instance_presenter.thumbnail
|
||||
- description ||= strip_tags(@instance_presenter.description.presence || t('about.about_mastodon_html'))
|
||||
- description ||= @instance_presenter.description.presence || strip_tags(t('about.about_mastodon_html'))
|
||||
|
||||
%meta{ name: 'description', content: description }/
|
||||
|
||||
|
|
|
@ -55,5 +55,5 @@
|
|||
%tbody
|
||||
%tr
|
||||
%td.button-primary
|
||||
= link_to full_asset_url(@backup.dump.url) do
|
||||
= link_to download_backup_url(@backup) do
|
||||
%span= t 'exports.archive_takeout.download'
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
|
||||
<%= t 'user_mailer.backup_ready.explanation' %>
|
||||
|
||||
=> <%= full_asset_url(@backup.dump.url) %>
|
||||
=> <%= download_backup_url(@backup) %>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ActivityPub::MigratedFollowDeliveryWorker < ActivityPub::DeliveryWorker
|
||||
def perform(json, source_account_id, inbox_url, old_target_account_id, options = {})
|
||||
super(json, source_account_id, inbox_url, options)
|
||||
unfollow_old_account!(old_target_account_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def unfollow_old_account!(old_target_account_id)
|
||||
old_target_account = Account.find(old_target_account_id)
|
||||
UnfollowService.new.call(@source_account, old_target_account, skip_unmerge: true)
|
||||
rescue StandardError
|
||||
true
|
||||
end
|
||||
end
|
|
@ -10,13 +10,7 @@ class UnfollowFollowWorker
|
|||
old_target_account = Account.find(old_target_account_id)
|
||||
new_target_account = Account.find(new_target_account_id)
|
||||
|
||||
follow = follower_account.active_relationships.find_by(target_account: old_target_account)
|
||||
reblogs = follow&.show_reblogs?
|
||||
notify = follow&.notify?
|
||||
languages = follow&.languages
|
||||
|
||||
FollowService.new.call(follower_account, new_target_account, reblogs: reblogs, notify: notify, languages: languages, bypass_locked: bypass_locked, bypass_limit: true)
|
||||
UnfollowService.new.call(follower_account, old_target_account, skip_unmerge: true)
|
||||
FollowMigrationService.new.call(follower_account, new_target_account, old_target_account, bypass_locked: bypass_locked)
|
||||
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError
|
||||
true
|
||||
end
|
||||
|
|
|
@ -5,7 +5,9 @@ require_relative '../config/boot'
|
|||
require_relative '../lib/cli'
|
||||
|
||||
begin
|
||||
Mastodon::CLI.start(ARGV)
|
||||
Chewy.strategy(:mastodon) do
|
||||
Mastodon::CLI.start(ARGV)
|
||||
end
|
||||
rescue Interrupt
|
||||
exit(130)
|
||||
end
|
||||
|
|
|
@ -39,6 +39,7 @@ require_relative '../lib/mastodon/rack_middleware'
|
|||
require_relative '../lib/devise/two_factor_ldap_authenticatable'
|
||||
require_relative '../lib/devise/two_factor_pam_authenticatable'
|
||||
require_relative '../lib/chewy/strategy/mastodon'
|
||||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
||||
require_relative '../lib/webpacker/manifest_extensions'
|
||||
require_relative '../lib/webpacker/helper_extensions'
|
||||
require_relative '../lib/rails/engine_extensions'
|
||||
|
|
|
@ -4,6 +4,7 @@ default: &default
|
|||
timeout: 5000
|
||||
encoding: unicode
|
||||
sslmode: <%= ENV['DB_SSLMODE'] || "prefer" %>
|
||||
application_name: ''
|
||||
|
||||
development:
|
||||
<<: *default
|
||||
|
|
|
@ -19,7 +19,7 @@ Chewy.settings = {
|
|||
# cycle, which takes care of checking if Elasticsearch is enabled
|
||||
# or not. However, mind that for the Rails console, the :urgent
|
||||
# strategy is set automatically with no way to override it.
|
||||
Chewy.root_strategy = :mastodon
|
||||
Chewy.root_strategy = :bypass_with_warning if Rails.env.production?
|
||||
Chewy.request_strategy = :mastodon
|
||||
Chewy.use_after_commit_callbacks = false
|
||||
|
||||
|
|
|
@ -124,6 +124,7 @@ elsif ENV['SWIFT_ENABLED'] == 'true'
|
|||
openstack_domain_name: ENV.fetch('SWIFT_DOMAIN_NAME') { 'default' },
|
||||
openstack_region: ENV['SWIFT_REGION'],
|
||||
openstack_cache_ttl: ENV.fetch('SWIFT_CACHE_TTL') { 60 },
|
||||
openstack_temp_url_key: ENV['SWIFT_TEMP_URL_KEY'],
|
||||
},
|
||||
|
||||
fog_file: { 'Cache-Control' => 'public, max-age=315576000, immutable' },
|
||||
|
|
|
@ -836,6 +836,12 @@ en:
|
|||
message_html: You haven't defined any server rules.
|
||||
sidekiq_process_check:
|
||||
message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration
|
||||
upload_check_privacy_error:
|
||||
action: Check here for more information
|
||||
message_html: "<strong>Your web server is misconfigured. The privacy of your users is at risk.</strong>"
|
||||
upload_check_privacy_error_object_storage:
|
||||
action: Check here for more information
|
||||
message_html: "<strong>Your object storage is misconfigured. The privacy of your users is at risk.</strong>"
|
||||
tags:
|
||||
review: Review status
|
||||
updated_msg: Hashtag settings updated successfully
|
||||
|
@ -1423,6 +1429,7 @@ en:
|
|||
relationships:
|
||||
activity: Account activity
|
||||
dormant: Dormant
|
||||
follow_failure: Could not follow some of the selected accounts.
|
||||
follow_selected_followers: Follow selected followers
|
||||
followers: Followers
|
||||
following: Following
|
||||
|
|
|
@ -109,6 +109,8 @@ Rails.application.routes.draw do
|
|||
|
||||
resource :inbox, only: [:create], module: :activitypub
|
||||
|
||||
get '/:encoded_at(*path)', to: redirect("/@%{path}"), constraints: { encoded_at: /%40/ }
|
||||
|
||||
constraints(username: /[^@\/.]+/) do
|
||||
get '/@:username', to: 'accounts#show', as: :short_account
|
||||
get '/@:username/with_replies', to: 'accounts#show', as: :short_account_with_replies
|
||||
|
@ -217,6 +219,7 @@ Rails.application.routes.draw do
|
|||
resource :statuses_cleanup, controller: :statuses_cleanup, only: [:show, :update]
|
||||
|
||||
get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy, format: false
|
||||
get '/backups/:id/download', to: 'backups#download', as: :download_backup, format: false
|
||||
|
||||
resource :authorize_interaction, only: [:show, :create]
|
||||
resource :share, only: [:show, :create]
|
||||
|
@ -466,7 +469,9 @@ Rails.application.routes.draw do
|
|||
resources :list, only: :show
|
||||
end
|
||||
|
||||
resources :streaming, only: [:index]
|
||||
get '/streaming', to: 'streaming#index'
|
||||
get '/streaming/(*any)', to: 'streaming#index'
|
||||
|
||||
resources :custom_emojis, only: [:index]
|
||||
resources :suggestions, only: [:index, :destroy]
|
||||
resources :scheduled_statuses, only: [:index, :show, :update, :destroy]
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |seed|
|
||||
load seed
|
||||
Chewy.strategy(:mastodon) do
|
||||
Dir[Rails.root.join('db', 'seeds', '*.rb')].sort.each do |seed|
|
||||
load seed
|
||||
end
|
||||
end
|
||||
|
|
|
@ -56,7 +56,7 @@ services:
|
|||
|
||||
web:
|
||||
build: .
|
||||
image: tootsuite/mastodon:v3.5.5
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||
|
@ -77,7 +77,7 @@ services:
|
|||
|
||||
streaming:
|
||||
build: .
|
||||
image: tootsuite/mastodon:v3.5.5
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming
|
||||
|
@ -95,7 +95,7 @@ services:
|
|||
|
||||
sidekiq:
|
||||
build: .
|
||||
image: tootsuite/mastodon:v3.5.5
|
||||
image: ghcr.io/mastodon/mastodon
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chewy
|
||||
class Strategy
|
||||
class BypassWithWarning < Base
|
||||
def update(...)
|
||||
Rails.logger.warn 'Chewy update without a root strategy' unless @warning_issued
|
||||
@warning_issued = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -53,14 +53,16 @@ module Mastodon
|
|||
|
||||
progress.log("Processing #{item.id}") if options[:verbose]
|
||||
|
||||
result = ActiveRecord::Base.connection_pool.with_connection do
|
||||
yield(item)
|
||||
ensure
|
||||
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
||||
Thread.current[:redis] = nil
|
||||
end
|
||||
Chewy.strategy(:mastodon) do
|
||||
result = ActiveRecord::Base.connection_pool.with_connection do
|
||||
yield(item)
|
||||
ensure
|
||||
RedisConfiguration.pool.checkin if Thread.current[:redis]
|
||||
Thread.current[:redis] = nil
|
||||
end
|
||||
|
||||
aggregate.increment(result) if result.is_a?(Integer)
|
||||
aggregate.increment(result) if result.is_a?(Integer)
|
||||
end
|
||||
rescue => e
|
||||
progress.log pastel.red("Error processing #{item.id}: #{e}")
|
||||
ensure
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class Mastodon::SidekiqMiddleware
|
||||
BACKTRACE_LIMIT = 3
|
||||
|
||||
def call(*)
|
||||
yield
|
||||
def call(*, &block)
|
||||
Chewy.strategy(:mastodon, &block)
|
||||
rescue Mastodon::HostValidationError
|
||||
# Do not retry
|
||||
rescue => e
|
||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
|||
end
|
||||
|
||||
def patch
|
||||
2
|
||||
4
|
||||
end
|
||||
|
||||
def flags
|
||||
|
|
|
@ -55,7 +55,7 @@ describe RelationshipsController do
|
|||
end
|
||||
|
||||
context 'when select parameter is provided' do
|
||||
subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, block_domains: '' } }
|
||||
subject { patch :update, params: { form_account_batch: { account_ids: [poopfeast.id] }, remove_domains_from_followers: '' } }
|
||||
|
||||
it 'soft-blocks followers from selected domains' do
|
||||
poopfeast.follow!(user.account)
|
||||
|
@ -66,6 +66,15 @@ describe RelationshipsController do
|
|||
expect(poopfeast.following?(user.account)).to be false
|
||||
end
|
||||
|
||||
it 'does not unfollow users from selected domains' do
|
||||
user.account.follow!(poopfeast)
|
||||
|
||||
sign_in user, scope: :user
|
||||
subject
|
||||
|
||||
expect(user.account.following?(poopfeast)).to be true
|
||||
end
|
||||
|
||||
include_examples 'authenticate user'
|
||||
include_examples 'redirects back to followers page'
|
||||
end
|
||||
|
|
|
@ -248,7 +248,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: 'USB Key' }
|
||||
|
||||
expect(response).to have_http_status(500)
|
||||
expect(response).to have_http_status(422)
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
|
@ -268,7 +268,7 @@ describe Settings::TwoFactorAuthentication::WebauthnCredentialsController do
|
|||
|
||||
post :create, params: { credential: new_webauthn_credential, nickname: nickname }
|
||||
|
||||
expect(response).to have_http_status(500)
|
||||
expect(response).to have_http_status(422)
|
||||
expect(flash[:error]).to be_present
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ RSpec.describe PlainTextFormatter do
|
|||
describe '#to_s' do
|
||||
subject { described_class.new(status.text, status.local?).to_s }
|
||||
|
||||
context 'given a post with local status' do
|
||||
context 'when status is local' do
|
||||
let(:status) { Fabricate(:status, text: '<p>a text by a nerd who uses an HTML tag in text</p>', uri: nil) }
|
||||
|
||||
it 'returns the raw text' do
|
||||
|
@ -12,12 +12,63 @@ RSpec.describe PlainTextFormatter do
|
|||
end
|
||||
end
|
||||
|
||||
context 'given a post with remote status' do
|
||||
context 'when status is remote' do
|
||||
let(:remote_account) { Fabricate(:account, domain: 'remote.test', username: 'bob', url: 'https://remote.test/') }
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Hello</p><script>alert("Hello")</script>') }
|
||||
|
||||
it 'returns tag-stripped text' do
|
||||
is_expected.to eq 'Hello'
|
||||
context 'when text contains inline HTML tags' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: '<b>Lorem</b> <em>ipsum</em>') }
|
||||
|
||||
it 'strips the tags' do
|
||||
expect(subject).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains <p> tags' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: '<p>Lorem</p><p>ipsum</p>') }
|
||||
|
||||
it 'inserts a newline' do
|
||||
expect(subject).to eq "Lorem\nipsum"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains a single <br> tag' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem<br>ipsum') }
|
||||
|
||||
it 'inserts a newline' do
|
||||
expect(subject).to eq "Lorem\nipsum"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains consecutive <br> tag' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem<br><br><br>ipsum') }
|
||||
|
||||
it 'inserts a single newline' do
|
||||
expect(subject).to eq "Lorem\nipsum"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains HTML entity' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem & ipsum ❤') }
|
||||
|
||||
it 'unescapes the entity' do
|
||||
expect(subject).to eq 'Lorem & ipsum ❤'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains <script> tag' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem <script> alert("Booh!") </script>ipsum') }
|
||||
|
||||
it 'strips the tag and its contents' do
|
||||
expect(subject).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when text contains an HTML comment tags' do
|
||||
let(:status) { Fabricate(:status, account: remote_account, text: 'Lorem <!-- Booh! -->ipsum') }
|
||||
|
||||
it 'strips the comment' do
|
||||
expect(subject).to eq 'Lorem ipsum'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue