Merge security fixes of v3.5.17 (#1341)
_todo_ --------- Co-authored-by: Claire <claire.github-309c@sitedethib.com> Co-authored-by: Essem <smswessem@gmail.com> Co-authored-by: Jakob Gillich <jakob@gillich.me> Co-authored-by: David Aaron <1858430+suddjian@users.noreply.github.com> Co-authored-by: Matt Jankowski <matt@jankowski.online> Co-authored-by: Jonathan de Jong <jonathandejong02@gmail.com>
This commit is contained in:
parent
1eaaff303c
commit
3fd984f95c
38
CHANGELOG.md
38
CHANGELOG.md
|
@ -5,8 +5,42 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
## End of life notice
|
## End of life notice
|
||||||
|
|
||||||
**The 3.5.x branch will not receive any update after 2023-12-31.**
|
**The 3.5.x branch has reached its end of life and will not receive any further update.**
|
||||||
This means that no security fix will be made available for this branch after this date, and you will need to update to a more recent version (such as the 4.1.x branch) to receive security fixes.
|
This means that no security fix will be made available for this branch after this date, and you will need to update to a more recent version (such as the 4.2.x branch) to receive security fixes.
|
||||||
|
|
||||||
|
## [3.5.17] - 2024-02-01
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Fix TODO
|
||||||
|
|
||||||
|
## [3.5.16] - 2023-12-04
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change GIF max matrix size error to explicitly mention GIF files ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27927))
|
||||||
|
- Change `Follow` activities delivery to bypass availability check ([ShadowJonathan](https://github.com/mastodon/mastodon/pull/27586))
|
||||||
|
- Change Content-Security-Policy to be tighter on media paths ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26889))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix incoming status creation date not being restricted to standard ISO8601 ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27655), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/28081))
|
||||||
|
- Fix posts from force-sensitized accounts being able to trend ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27620))
|
||||||
|
- Fix processing LDSigned activities from actors with unknown public keys ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27474))
|
||||||
|
|
||||||
|
## [3.5.15] - 2023-10-10
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Change user archive export allowed period from 7 days to 6 days ([suddjian](https://github.com/mastodon/mastodon/pull/27200))
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix mentions being matched in some URL query strings ([mjankowski](https://github.com/mastodon/mastodon/pull/25656))
|
||||||
|
- Fix importer returning negative row estimates ([jgillich](https://github.com/mastodon/mastodon/pull/27258))
|
||||||
|
- Fix filtering audit log for entries about disabling 2FA ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27186))
|
||||||
|
- Fix tIME chunk not being properly removed from PNG uploads ([TheEssem](https://github.com/mastodon/mastodon/pull/27111))
|
||||||
|
- Fix inefficient queries in “Follows and followers” as well as several admin pages ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/27116), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/27306))
|
||||||
|
|
||||||
## [3.5.14] - 2023-09-19
|
## [3.5.14] - 2023-09-19
|
||||||
|
|
||||||
|
|
15
SECURITY.md
15
SECURITY.md
|
@ -10,11 +10,10 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through
|
||||||
|
|
||||||
## Supported Versions
|
## Supported Versions
|
||||||
|
|
||||||
| Version | Supported |
|
| Version | Supported |
|
||||||
| ------- | ------------------ |
|
| ------- | ---------------- |
|
||||||
| 3.5.x | Until 2023-12-31 |
|
| 4.2.x | Yes |
|
||||||
| 3.4.x | No |
|
| 4.1.x | Yes |
|
||||||
| 3.3.x | No |
|
| 4.0.x | No |
|
||||||
| < 3.3 | No |
|
| 3.5.x | No |
|
||||||
|
| < 3.5 | No |
|
||||||
[bug-bounty]: https://app.intigriti.com/programs/mastodon/mastodonio/detail
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AccountsIndex < Chewy::Index
|
class AccountsIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
|
@ -38,6 +40,6 @@ class AccountsIndex < Chewy::Index
|
||||||
|
|
||||||
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
field :following_count, type: 'long', value: ->(account) { account.following_count }
|
||||||
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
field :followers_count, type: 'long', value: ->(account) { account.followers_count }
|
||||||
field :last_status_at, type: 'date', value: ->(account) { account.last_status_at || account.created_at }
|
field :last_status_at, type: 'date', value: ->(account) { clamp_date(account.last_status_at || account.created_at) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module DatetimeClampingConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
MIN_ISO8601_DATETIME = '0000-01-01T00:00:00Z'.to_datetime.freeze
|
||||||
|
MAX_ISO8601_DATETIME = '9999-12-31T23:59:59Z'.to_datetime.freeze
|
||||||
|
|
||||||
|
class_methods do
|
||||||
|
def clamp_date(datetime)
|
||||||
|
datetime.clamp(MIN_ISO8601_DATETIME, MAX_ISO8601_DATETIME)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TagsIndex < Chewy::Index
|
class TagsIndex < Chewy::Index
|
||||||
|
include DatetimeClampingConcern
|
||||||
|
|
||||||
settings index: { refresh_interval: '30s' }, analysis: {
|
settings index: { refresh_interval: '30s' }, analysis: {
|
||||||
analyzer: {
|
analyzer: {
|
||||||
content: {
|
content: {
|
||||||
|
@ -36,6 +38,6 @@ class TagsIndex < Chewy::Index
|
||||||
|
|
||||||
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
field :reviewed, type: 'boolean', value: ->(tag) { tag.reviewed? }
|
||||||
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
field :usage, type: 'long', value: ->(tag, crutches) { tag.history.aggregate(crutches.time_period).accounts }
|
||||||
field :last_status_at, type: 'date', value: ->(tag) { tag.last_status_at || tag.created_at }
|
field :last_status_at, type: 'date', value: ->(tag) { clamp_date(tag.last_status_at || tag.created_at) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,8 +18,8 @@ class ActivityPub::LinkedDataSignature
|
||||||
|
|
||||||
return unless type == 'RsaSignature2017'
|
return unless type == 'RsaSignature2017'
|
||||||
|
|
||||||
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
|
creator = ActivityPub::TagManager.instance.uri_to_resource(creator_uri, Account)
|
||||||
creator ||= ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false)
|
creator = ActivityPub::FetchRemoteKeyService.new.call(creator_uri, id: false) if creator&.public_key.blank?
|
||||||
|
|
||||||
return if creator.nil?
|
return if creator.nil?
|
||||||
|
|
||||||
|
@ -27,9 +27,9 @@ class ActivityPub::LinkedDataSignature
|
||||||
document_hash = hash(@json.without('signature'))
|
document_hash = hash(@json.without('signature'))
|
||||||
to_be_verified = options_hash + document_hash
|
to_be_verified = options_hash + document_hash
|
||||||
|
|
||||||
if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
|
creator if creator.keypair.public_key.verify(OpenSSL::Digest.new('SHA256'), Base64.decode64(signature), to_be_verified)
|
||||||
creator
|
rescue OpenSSL::PKey::RSAError
|
||||||
end
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
def sign!(creator, sign_with: nil)
|
def sign!(creator, sign_with: nil)
|
||||||
|
|
|
@ -53,7 +53,8 @@ class ActivityPub::Parser::StatusParser
|
||||||
end
|
end
|
||||||
|
|
||||||
def created_at
|
def created_at
|
||||||
@object['published']&.to_datetime
|
datetime = @object['published']&.to_datetime
|
||||||
|
datetime if datetime.present? && (0..9999).cover?(datetime.year)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,9 @@ class Importer::BaseImporter
|
||||||
# Estimate the amount of documents that would be indexed. Not exact!
|
# Estimate the amount of documents that would be indexed. Not exact!
|
||||||
# @returns [Integer]
|
# @returns [Integer]
|
||||||
def estimate!
|
def estimate!
|
||||||
ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples AS estimate FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['estimate'].to_i }
|
reltuples = ActiveRecord::Base.connection_pool.with_connection { |connection| connection.select_one("SELECT reltuples FROM pg_class WHERE relname = '#{index.adapter.target.table_name}'")['reltuples'].to_i }
|
||||||
|
# If the table has never yet been vacuumed or analyzed, reltuples contains -1
|
||||||
|
[reltuples, 0].max
|
||||||
end
|
end
|
||||||
|
|
||||||
# Import data from the database into the index
|
# Import data from the database into the index
|
||||||
|
|
|
@ -61,9 +61,9 @@ class Account < ApplicationRecord
|
||||||
trust_level
|
trust_level
|
||||||
)
|
)
|
||||||
|
|
||||||
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
|
USERNAME_RE = /[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?/i
|
||||||
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[[:word:]]+)?)/i
|
MENTION_RE = %r{(?<![=/[:word:]])@((#{USERNAME_RE})(?:@[[:word:].-]+[[:word:]]+)?)}i
|
||||||
URL_PREFIX_RE = /\Ahttp(s?):\/\/[^\/]+/
|
URL_PREFIX_RE = %r{\Ahttp(s?)://[^/]+}
|
||||||
|
|
||||||
include Attachmentable
|
include Attachmentable
|
||||||
include AccountAssociations
|
include AccountAssociations
|
||||||
|
@ -113,8 +113,8 @@ class Account < ApplicationRecord
|
||||||
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
scope :searchable, -> { without_unapproved.without_suspended.where(moved_to_account_id: nil) }
|
||||||
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
|
||||||
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
scope :followable_by, ->(account) { joins(arel_table.join(Follow.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(Follow.arel_table[:target_account_id]).and(Follow.arel_table[:account_id].eq(account.id))).join_sources).where(Follow.arel_table[:id].eq(nil)).joins(arel_table.join(FollowRequest.arel_table, Arel::Nodes::OuterJoin).on(arel_table[:id].eq(FollowRequest.arel_table[:target_account_id]).and(FollowRequest.arel_table[:account_id].eq(account.id))).join_sources).where(FollowRequest.arel_table[:id].eq(nil)) }
|
||||||
scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
|
scope :by_recent_status, -> { includes(:account_stat).merge(AccountStat.order('last_status_at DESC NULLS LAST')).references(:account_stat) }
|
||||||
scope :by_recent_sign_in, -> { order(Arel.sql('(case when users.current_sign_in_at is null then 1 else 0 end) asc, users.current_sign_in_at desc, accounts.id desc')) }
|
scope :by_recent_sign_in, -> { order(Arel.sql('users.current_sign_in_at DESC NULLS LAST')) }
|
||||||
scope :popular, -> { order('account_stats.followers_count desc') }
|
scope :popular, -> { order('account_stats.followers_count desc') }
|
||||||
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
|
||||||
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Admin::ActionLogFilter
|
||||||
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
destroy_instance: { target_type: 'Instance', action: 'destroy' }.freeze,
|
||||||
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
|
destroy_unavailable_domain: { target_type: 'UnavailableDomain', action: 'destroy' }.freeze,
|
||||||
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
|
destroy_status: { target_type: 'Status', action: 'destroy' }.freeze,
|
||||||
disable_2fa_user: { target_type: 'User', action: 'disable' }.freeze,
|
disable_2fa_user: { target_type: 'User', action: 'disable_2fa' }.freeze,
|
||||||
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
|
disable_custom_emoji: { target_type: 'CustomEmoji', action: 'disable' }.freeze,
|
||||||
disable_user: { target_type: 'User', action: 'disable' }.freeze,
|
disable_user: { target_type: 'User', action: 'disable' }.freeze,
|
||||||
enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
|
enable_custom_emoji: { target_type: 'CustomEmoji', action: 'enable' }.freeze,
|
||||||
|
|
|
@ -52,9 +52,13 @@ module Attachmentable
|
||||||
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
|
return if attachment.blank? || !/image.*/.match?(attachment.content_type) || attachment.queued_for_write[:original].blank?
|
||||||
|
|
||||||
width, height = FastImage.size(attachment.queued_for_write[:original].path)
|
width, height = FastImage.size(attachment.queued_for_write[:original].path)
|
||||||
matrix_limit = attachment.content_type == 'image/gif' ? GIF_MATRIX_LIMIT : MAX_MATRIX_LIMIT
|
return unless width.present? && height.present?
|
||||||
|
|
||||||
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported" if width.present? && height.present? && (width * height > matrix_limit)
|
if attachment.content_type == 'image/gif' && width * height > GIF_MATRIX_LIMIT
|
||||||
|
raise Mastodon::DimensionsValidationError, "#{width}x#{height} GIF files are not supported"
|
||||||
|
elsif width * height > MAX_MATRIX_LIMIT
|
||||||
|
raise Mastodon::DimensionsValidationError, "#{width}x#{height} images are not supported"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def appropriate_extension(attachment)
|
def appropriate_extension(attachment)
|
||||||
|
|
|
@ -156,7 +156,7 @@ class MediaAttachment < ApplicationRecord
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
GLOBAL_CONVERT_OPTIONS = {
|
GLOBAL_CONVERT_OPTIONS = {
|
||||||
all: '-quality 90 -strip +set modify-date +set create-date',
|
all: '-quality 90 -strip +set date:modify +set date:create +set date:timestamp',
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
belongs_to :account, inverse_of: :media_attachments, optional: true
|
belongs_to :account, inverse_of: :media_attachments, optional: true
|
||||||
|
|
|
@ -60,13 +60,13 @@ class RelationshipFilter
|
||||||
def relationship_scope(value)
|
def relationship_scope(value)
|
||||||
case value
|
case value
|
||||||
when 'following'
|
when 'following'
|
||||||
account.following.eager_load(:account_stat).reorder(nil)
|
account.following.includes(:account_stat).reorder(nil)
|
||||||
when 'followed_by'
|
when 'followed_by'
|
||||||
account.followers.eager_load(:account_stat).reorder(nil)
|
account.followers.includes(:account_stat).reorder(nil)
|
||||||
when 'mutual'
|
when 'mutual'
|
||||||
account.followers.eager_load(:account_stat).reorder(nil).merge(Account.where(id: account.following))
|
account.followers.includes(:account_stat).reorder(nil).merge(Account.where(id: account.following))
|
||||||
when 'invited'
|
when 'invited'
|
||||||
Account.joins(user: :invite).merge(Invite.where(user: account.user)).eager_load(:account_stat).reorder(nil)
|
Account.joins(user: :invite).merge(Invite.where(user: account.user)).includes(:account_stat).reorder(nil)
|
||||||
else
|
else
|
||||||
raise "Unknown relationship: #{value}"
|
raise "Unknown relationship: #{value}"
|
||||||
end
|
end
|
||||||
|
@ -112,7 +112,7 @@ class RelationshipFilter
|
||||||
def activity_scope(value)
|
def activity_scope(value)
|
||||||
case value
|
case value
|
||||||
when 'dormant'
|
when 'dormant'
|
||||||
AccountStat.where(last_status_at: nil).or(AccountStat.where(AccountStat.arel_table[:last_status_at].lt(1.month.ago)))
|
Account.joins(:account_stat).where(account_stat: { last_status_at: [nil, ...1.month.ago] })
|
||||||
else
|
else
|
||||||
raise "Unknown activity: #{value}"
|
raise "Unknown activity: #{value}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -75,7 +75,7 @@ class Trends::Statuses < Trends::Base
|
||||||
private
|
private
|
||||||
|
|
||||||
def eligible?(status)
|
def eligible?(status)
|
||||||
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && !status.sensitive? && !status.reply?
|
status.public_visibility? && status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? && status.spoiler_text.blank? && !status.sensitive? && !status.reply?
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate_scores(statuses, at_time)
|
def calculate_scores(statuses, at_time)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class BackupPolicy < ApplicationPolicy
|
class BackupPolicy < ApplicationPolicy
|
||||||
MIN_AGE = 1.week
|
MIN_AGE = 6.days
|
||||||
|
|
||||||
def create?
|
def create?
|
||||||
user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero?
|
user_signed_in? && current_user.backups.where('created_at >= ?', MIN_AGE.ago).count.zero?
|
||||||
|
|
|
@ -70,7 +70,7 @@ class FollowService < BaseService
|
||||||
if @target_account.local?
|
if @target_account.local?
|
||||||
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
|
LocalNotificationWorker.perform_async(@target_account.id, follow_request.id, follow_request.class.name, 'follow_request')
|
||||||
elsif @target_account.activitypub?
|
elsif @target_account.activitypub?
|
||||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(build_json(follow_request), @source_account.id, @target_account.inbox_url, { 'bypass_availability' => true })
|
||||||
end
|
end
|
||||||
|
|
||||||
follow_request
|
follow_request
|
||||||
|
|
|
@ -13,9 +13,10 @@ class ActivityPub::DeliveryWorker
|
||||||
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
HEADERS = { 'Content-Type' => 'application/activity+json' }.freeze
|
||||||
|
|
||||||
def perform(json, source_account_id, inbox_url, options = {})
|
def perform(json, source_account_id, inbox_url, options = {})
|
||||||
return unless DeliveryFailureTracker.available?(inbox_url)
|
|
||||||
|
|
||||||
@options = options.with_indifferent_access
|
@options = options.with_indifferent_access
|
||||||
|
|
||||||
|
return unless @options[:bypass_availability] || DeliveryFailureTracker.available?(inbox_url)
|
||||||
|
|
||||||
@json = json
|
@json = json
|
||||||
@source_account = Account.find(source_account_id)
|
@source_account = Account.find(source_account_id)
|
||||||
@inbox_url = inbox_url
|
@inbox_url = inbox_url
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
|
||||||
|
|
||||||
def host_to_url(str)
|
def host_to_url(str)
|
||||||
"http#{Rails.configuration.x.use_https ? 's' : ''}://#{str.split('/').first}" if str.present?
|
return if str.blank?
|
||||||
|
|
||||||
|
uri = Addressable::URI.parse("http#{Rails.configuration.x.use_https ? 's' : ''}://#{str}")
|
||||||
|
uri.path += '/' unless uri.path.blank? || uri.path.end_with?('/')
|
||||||
|
uri.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
base_host = Rails.configuration.x.web_domain
|
base_host = Rails.configuration.x.web_domain
|
||||||
|
|
|
@ -44,7 +44,7 @@ services:
|
||||||
|
|
||||||
web:
|
web:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v3.5.14
|
image: ghcr.io/mastodon/mastodon:v3.5.17
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"
|
||||||
|
@ -65,7 +65,7 @@ services:
|
||||||
|
|
||||||
streaming:
|
streaming:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v3.5.14
|
image: ghcr.io/mastodon/mastodon:v3.5.17
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: node ./streaming
|
command: node ./streaming
|
||||||
|
@ -83,7 +83,7 @@ services:
|
||||||
|
|
||||||
sidekiq:
|
sidekiq:
|
||||||
build: .
|
build: .
|
||||||
image: ghcr.io/mastodon/mastodon:v3.5.14
|
image: ghcr.io/mastodon/mastodon:v3.5.17
|
||||||
restart: always
|
restart: always
|
||||||
env_file: .env.production
|
env_file: .env.production
|
||||||
command: bundle exec sidekiq
|
command: bundle exec sidekiq
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Mastodon
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch
|
def patch
|
||||||
14
|
17
|
||||||
end
|
end
|
||||||
|
|
||||||
def flags
|
def flags
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
Fabricator(:account_stat) do
|
Fabricator(:account_stat) do
|
||||||
account nil
|
account { Fabricate.build(:account) }
|
||||||
statuses_count ""
|
statuses_count '123'
|
||||||
following_count ""
|
following_count '456'
|
||||||
followers_count ""
|
followers_count '789'
|
||||||
end
|
end
|
||||||
|
|
|
@ -29,7 +29,47 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
subject.perform
|
subject.perform
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'object has been edited' do
|
context 'when object publication date is below ISO8601 range' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
published: '-0977-11-03T08:31:22Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status with a valid creation date', :aggregate_failures do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
|
||||||
|
expect(status.created_at).to be_within(30).of(Time.now.utc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when object publication date is above ISO8601 range' do
|
||||||
|
let(:object_json) do
|
||||||
|
{
|
||||||
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
type: 'Note',
|
||||||
|
content: 'Lorem ipsum',
|
||||||
|
published: '10000-11-03T08:31:22Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates status with a valid creation date', :aggregate_failures do
|
||||||
|
status = sender.statuses.first
|
||||||
|
|
||||||
|
expect(status).to_not be_nil
|
||||||
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
|
|
||||||
|
expect(status.created_at).to be_within(30).of(Time.now.utc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when object has been edited' do
|
||||||
let(:object_json) do
|
let(:object_json) do
|
||||||
{
|
{
|
||||||
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
id: [ActivityPub::TagManager.instance.uri_for(sender), '#bar'].join,
|
||||||
|
@ -40,18 +80,16 @@ RSpec.describe ActivityPub::Activity::Create do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates status' do
|
it 'creates status with appropriate creation and edition dates', :aggregate_failures do
|
||||||
status = sender.statuses.first
|
status = sender.statuses.first
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status).to_not be_nil
|
||||||
expect(status.text).to eq 'Lorem ipsum'
|
expect(status.text).to eq 'Lorem ipsum'
|
||||||
end
|
|
||||||
|
|
||||||
it 'marks status as edited' do
|
expect(status.created_at).to eq '2022-01-22T15:00:00Z'.to_datetime
|
||||||
status = sender.statuses.first
|
|
||||||
|
|
||||||
expect(status).to_not be_nil
|
expect(status.edited?).to be true
|
||||||
expect(status.edited?).to eq true
|
expect(status.edited_at).to eq '2022-01-22T16:00:00Z'.to_datetime
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,40 @@ RSpec.describe ActivityPub::LinkedDataSignature do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when local account record is missing a public key' do
|
||||||
|
let(:raw_signature) do
|
||||||
|
{
|
||||||
|
'creator' => 'http://example.com/alice',
|
||||||
|
'created' => '2017-09-23T20:21:34Z',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:signature) { raw_signature.merge('type' => 'RsaSignature2017', 'signatureValue' => sign(sender, raw_signature, raw_json)) }
|
||||||
|
|
||||||
|
let(:service_stub) { instance_double(ActivityPub::FetchRemoteKeyService) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# Ensure signature is computed with the old key
|
||||||
|
signature
|
||||||
|
|
||||||
|
# Unset key
|
||||||
|
old_key = sender.public_key
|
||||||
|
sender.update!(private_key: '', public_key: '')
|
||||||
|
|
||||||
|
allow(ActivityPub::FetchRemoteKeyService).to receive(:new).and_return(service_stub)
|
||||||
|
|
||||||
|
allow(service_stub).to receive(:call).with('http://example.com/alice', id: false) do
|
||||||
|
sender.update!(public_key: old_key)
|
||||||
|
sender
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'fetches key and returns creator' do
|
||||||
|
expect(subject.verify_account!).to eq sender
|
||||||
|
expect(service_stub).to have_received(:call).with('http://example.com/alice', id: false).once
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when signature is missing' do
|
context 'when signature is missing' do
|
||||||
let(:signature) { nil }
|
let(:signature) { nil }
|
||||||
|
|
||||||
|
|
|
@ -689,7 +689,7 @@ RSpec.describe Account, type: :model do
|
||||||
expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
|
expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil
|
||||||
end
|
end
|
||||||
|
|
||||||
xit 'does not match URL querystring' do
|
it 'does not match URL query string' do
|
||||||
expect(subject.match('https://example.com/?x=@alice')).to be_nil
|
expect(subject.match('https://example.com/?x=@alice')).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,32 +6,60 @@ describe RelationshipFilter do
|
||||||
let(:account) { Fabricate(:account) }
|
let(:account) { Fabricate(:account) }
|
||||||
|
|
||||||
describe '#results' do
|
describe '#results' do
|
||||||
context 'when default params are used' do
|
let(:account_of_7_months) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 7.months.ago).account }
|
||||||
let(:subject) do
|
let(:account_of_1_day) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 1.day.ago).account }
|
||||||
RelationshipFilter.new(account, 'order' => 'active').results
|
let(:account_of_3_days) { Fabricate(:account_stat, statuses_count: 1, last_status_at: 3.days.ago).account }
|
||||||
|
let(:silent_account) { Fabricate(:account_stat, statuses_count: 0, last_status_at: nil).account }
|
||||||
|
|
||||||
|
before do
|
||||||
|
account.follow!(account_of_7_months)
|
||||||
|
account.follow!(account_of_1_day)
|
||||||
|
account.follow!(account_of_3_days)
|
||||||
|
account.follow!(silent_account)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ordering by last activity' do
|
||||||
|
context 'when not filtering' do
|
||||||
|
subject do
|
||||||
|
described_class.new(account, 'order' => 'active').results
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns followings ordered by last activity' do
|
||||||
|
expect(subject).to eq [account_of_1_day, account_of_3_days, account_of_7_months, silent_account]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
context 'when filtering for dormant accounts' do
|
||||||
add_following_account_with(last_status_at: 7.days.ago)
|
subject do
|
||||||
add_following_account_with(last_status_at: 1.day.ago)
|
described_class.new(account, 'order' => 'active', 'activity' => 'dormant').results
|
||||||
add_following_account_with(last_status_at: 3.days.ago)
|
end
|
||||||
|
|
||||||
|
it 'returns dormant followings ordered by last activity' do
|
||||||
|
expect(subject).to eq [account_of_7_months, silent_account]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when ordering by account creation' do
|
||||||
|
context 'when not filtering' do
|
||||||
|
subject do
|
||||||
|
described_class.new(account, 'order' => 'recent').results
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns followings ordered by last account creation' do
|
||||||
|
expect(subject).to eq [silent_account, account_of_3_days, account_of_1_day, account_of_7_months]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns followings ordered by last activity' do
|
context 'when filtering for dormant accounts' do
|
||||||
expected_result = account.following.eager_load(:account_stat).reorder(nil).by_recent_status
|
subject do
|
||||||
|
described_class.new(account, 'order' => 'recent', 'activity' => 'dormant').results
|
||||||
|
end
|
||||||
|
|
||||||
expect(subject).to eq expected_result
|
it 'returns dormant followings ordered by last activity' do
|
||||||
|
expect(subject).to eq [silent_account, account_of_7_months]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_following_account_with(last_status_at:)
|
|
||||||
following_account = Fabricate(:account)
|
|
||||||
Fabricate(:account_stat, account: following_account,
|
|
||||||
last_status_at: last_status_at,
|
|
||||||
statuses_count: 1,
|
|
||||||
following_count: 0,
|
|
||||||
followers_count: 0)
|
|
||||||
Fabricate(:follow, account: account, target_account: following_account).account
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue