Rebase to current 4.3.0-alpha-glitch-soc, update theming files
This commit is contained in:
1 parent
aeb49bcba0
commit
fff7926498
71 files changed
+30937
-2221
No files matched your search
@@ -0,0 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Admin::SystemCheck
|
||||
ACTIVE_CHECKS = [
|
||||
<<<<<<< HEAD
|
||||
Admin::SystemCheck::SoftwareVersionCheck,
|
||||
=======
|
||||
>>>>>>> 6a7b91a03 (Add warning for object storage misconfiguration (#24137))
|
||||
Admin::SystemCheck::MediaPrivacyCheck,
|
||||
Admin::SystemCheck::DatabaseSchemaCheck,
|
||||
Admin::SystemCheck::SidekiqProcessCheck,
|
||||
Admin::SystemCheck::RulesCheck,
|
||||
Admin::SystemCheck::ElasticsearchCheck,
|
||||
].freeze
|
||||
|
||||
def self.perform(current_user)
|
||||
ACTIVE_CHECKS.each_with_object([]) do |klass, arr|
|
||||
check = klass.new(current_user)
|
||||
|
||||
if check.skip? || check.pass?
|
||||
arr
|
||||
else
|
||||
arr << check.message
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class PlainTextFormatter
|
||||
NEWLINE_TAGS_RE = %r{(<br />|<br>|</p>)+}
|
||||
|
||||
attr_reader :text, :local
|
||||
|
||||
alias local? local
|
||||
|
||||
def initialize(text, local)
|
||||
@text = text
|
||||
@local = local
|
||||
end
|
||||
|
||||
def to_s
|
||||
if local?
|
||||
text
|
||||
else
|
||||
<<<<<<< HEAD
|
||||
node = Nokogiri::HTML.fragment(insert_newlines)
|
||||
# Elements that are entirely removed with our Sanitize config
|
||||
node.xpath('.//iframe|.//math|.//noembed|.//noframes|.//noscript|.//plaintext|.//script|.//style|.//svg|.//xmp').remove
|
||||
node.text.chomp
|
||||
=======
|
||||
html_entities.decode(strip_tags(insert_newlines)).chomp
|
||||
>>>>>>> 3f2e31800 (Unescape HTML entities (#24019))
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def insert_newlines
|
||||
text.gsub(NEWLINE_TAGS_RE) { |match| "#{match}\n" }
|
||||
end
|
||||
|
||||
def html_entities
|
||||
HTMLEntities.new
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,362 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'ipaddr'
|
||||
require 'socket'
|
||||
require 'resolv'
|
||||
|
||||
# Use our own timeout class to avoid using HTTP.rb's timeout block
|
||||
# around the Socket#open method, since we use our own timeout blocks inside
|
||||
# that method
|
||||
#
|
||||
# Also changes how the read timeout behaves so that it is cumulative (closer
|
||||
# to HTTP::Timeout::Global, but still having distinct timeouts for other
|
||||
# operation types)
|
||||
<<<<<<< HEAD
|
||||
class PerOperationWithDeadline < HTTP::Timeout::PerOperation
|
||||
READ_DEADLINE = 30
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
|
||||
@read_deadline = options.fetch(:read_deadline, READ_DEADLINE)
|
||||
end
|
||||
|
||||
=======
|
||||
class HTTP::Timeout::PerOperation
|
||||
>>>>>>> e75ad1de0 (Merge pull request from GHSA-9pxv-6qvf-pjwc)
|
||||
def connect(socket_class, host, port, nodelay = false)
|
||||
@socket = socket_class.open(host, port)
|
||||
@socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
|
||||
end
|
||||
|
||||
# Reset deadline when the connection is re-used for different requests
|
||||
def reset_counter
|
||||
@deadline = nil
|
||||
end
|
||||
|
||||
# Read data from the socket
|
||||
def readpartial(size, buffer = nil)
|
||||
<<<<<<< HEAD
|
||||
@deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_deadline
|
||||
=======
|
||||
@deadline ||= Process.clock_gettime(Process::CLOCK_MONOTONIC) + @read_timeout
|
||||
>>>>>>> e75ad1de0 (Merge pull request from GHSA-9pxv-6qvf-pjwc)
|
||||
|
||||
timeout = false
|
||||
loop do
|
||||
result = @socket.read_nonblock(size, buffer, exception: false)
|
||||
|
||||
return :eof if result.nil?
|
||||
|
||||
remaining_time = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
<<<<<<< HEAD
|
||||
raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout
|
||||
raise HTTP::TimeoutError, "Read timed out after a total of #{@read_deadline} seconds" if remaining_time <= 0
|
||||
=======
|
||||
raise HTTP::TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout || remaining_time <= 0
|
||||
>>>>>>> e75ad1de0 (Merge pull request from GHSA-9pxv-6qvf-pjwc)
|
||||
return result if result != :wait_readable
|
||||
|
||||
# marking the socket for timeout. Why is this not being raised immediately?
|
||||
# it seems there is some race-condition on the network level between calling
|
||||
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
|
||||
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
|
||||
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
|
||||
# also mean that the socket has been closed by the server. Therefore we "mark" the
|
||||
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
|
||||
# timeout. Else, the first timeout was a proper timeout.
|
||||
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
|
||||
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
|
||||
<<<<<<< HEAD
|
||||
timeout = true unless @socket.to_io.wait_readable([remaining_time, @read_timeout].min)
|
||||
=======
|
||||
timeout = true unless @socket.to_io.wait_readable(remaining_time)
|
||||
>>>>>>> e75ad1de0 (Merge pull request from GHSA-9pxv-6qvf-pjwc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Request
|
||||
REQUEST_TARGET = '(request-target)'
|
||||
|
||||
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
|
||||
# and 5s timeout on the TLS handshake, meaning the worst case should take
|
||||
# about 15s in total
|
||||
TIMEOUT = { connect_timeout: 5, read_timeout: 10, write_timeout: 10, read_deadline: 30 }.freeze
|
||||
|
||||
include RoutingHelper
|
||||
|
||||
def initialize(verb, url, **options)
|
||||
raise ArgumentError if url.blank?
|
||||
|
||||
@verb = verb
|
||||
@url = Addressable::URI.parse(url).normalize
|
||||
@http_client = options.delete(:http_client)
|
||||
@allow_local = options.delete(:allow_local)
|
||||
@options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
|
||||
@options = @options.merge(timeout_class: PerOperationWithDeadline, timeout_options: TIMEOUT)
|
||||
@options = @options.merge(proxy_url) if use_proxy?
|
||||
@headers = {}
|
||||
|
||||
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
|
||||
|
||||
set_common_headers!
|
||||
set_digest! if options.key?(:body)
|
||||
end
|
||||
|
||||
def on_behalf_of(actor, sign_with: nil)
|
||||
raise ArgumentError, 'actor must not be nil' if actor.nil?
|
||||
|
||||
@actor = actor
|
||||
@keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
def add_headers(new_headers)
|
||||
@headers.merge!(new_headers)
|
||||
self
|
||||
end
|
||||
|
||||
def perform
|
||||
begin
|
||||
response = http_client.request(@verb, @url.to_s, @options.merge(headers: headers))
|
||||
rescue => e
|
||||
raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
|
||||
end
|
||||
|
||||
begin
|
||||
# If we are using a persistent connection, we have to
|
||||
# read every response to be able to move forward at all.
|
||||
# However, simply calling #to_s or #flush may not be safe,
|
||||
# as the response body, if malicious, could be too big
|
||||
# for our memory. So we use the #body_with_limit method
|
||||
response.body_with_limit if http_client.persistent?
|
||||
|
||||
yield response if block_given?
|
||||
ensure
|
||||
http_client.close unless http_client.persistent?
|
||||
end
|
||||
end
|
||||
|
||||
def headers
|
||||
(@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
|
||||
end
|
||||
|
||||
class << self
|
||||
def valid_url?(url)
|
||||
begin
|
||||
parsed_url = Addressable::URI.parse(url)
|
||||
rescue Addressable::URI::InvalidURIError
|
||||
return false
|
||||
end
|
||||
|
||||
%w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
|
||||
end
|
||||
|
||||
def http_client
|
||||
HTTP.use(:auto_inflate).follow(max_hops: 3)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_common_headers!
|
||||
@headers[REQUEST_TARGET] = "#{@verb} #{@url.path}"
|
||||
@headers['User-Agent'] = Mastodon::Version.user_agent
|
||||
@headers['Host'] = @url.host
|
||||
@headers['Date'] = Time.now.utc.httpdate
|
||||
@headers['Accept-Encoding'] = 'gzip' if @verb != :head
|
||||
end
|
||||
|
||||
def set_digest!
|
||||
@headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
|
||||
end
|
||||
|
||||
def signature
|
||||
algorithm = 'rsa-sha256'
|
||||
signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
|
||||
|
||||
"keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
|
||||
end
|
||||
|
||||
def signed_string
|
||||
signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
|
||||
end
|
||||
|
||||
def signed_headers
|
||||
@headers.without('User-Agent', 'Accept-Encoding')
|
||||
end
|
||||
|
||||
def key_id
|
||||
ActivityPub::TagManager.instance.key_uri_for(@actor)
|
||||
end
|
||||
|
||||
def http_client
|
||||
@http_client ||= Request.http_client
|
||||
end
|
||||
|
||||
def use_proxy?
|
||||
proxy_url.present?
|
||||
end
|
||||
|
||||
def proxy_url
|
||||
if hidden_service? && Rails.configuration.x.http_client_hidden_proxy.present?
|
||||
Rails.configuration.x.http_client_hidden_proxy
|
||||
else
|
||||
Rails.configuration.x.http_client_proxy
|
||||
end
|
||||
end
|
||||
|
||||
def block_hidden_service?
|
||||
!Rails.configuration.x.access_to_hidden_service && hidden_service?
|
||||
end
|
||||
|
||||
def hidden_service?
|
||||
/\.(onion|i2p)$/.match?(@url.host)
|
||||
end
|
||||
|
||||
module ClientLimit
|
||||
def truncated_body(limit = 1.megabyte)
|
||||
if charset.nil?
|
||||
encoding = Encoding::BINARY
|
||||
else
|
||||
begin
|
||||
encoding = Encoding.find(charset)
|
||||
rescue ArgumentError
|
||||
encoding = Encoding::BINARY
|
||||
end
|
||||
end
|
||||
|
||||
contents = String.new(encoding: encoding)
|
||||
|
||||
while (chunk = readpartial)
|
||||
contents << chunk
|
||||
chunk.clear
|
||||
|
||||
break if contents.bytesize > limit
|
||||
end
|
||||
|
||||
contents
|
||||
end
|
||||
|
||||
def body_with_limit(limit = 1.megabyte)
|
||||
raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
|
||||
|
||||
contents = truncated_body(limit)
|
||||
raise Mastodon::LengthValidationError if contents.bytesize > limit
|
||||
|
||||
contents
|
||||
end
|
||||
end
|
||||
|
||||
if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production?
|
||||
abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied'
|
||||
else
|
||||
class ::HTTP::Response
|
||||
include Request::ClientLimit
|
||||
end
|
||||
end
|
||||
|
||||
class Socket < TCPSocket
|
||||
class << self
|
||||
def open(host, *args)
|
||||
outer_e = nil
|
||||
port = args.first
|
||||
|
||||
addresses = []
|
||||
begin
|
||||
addresses = [IPAddr.new(host)]
|
||||
rescue IPAddr::InvalidAddressError
|
||||
Resolv::DNS.open do |dns|
|
||||
dns.timeouts = 5
|
||||
addresses = dns.getaddresses(host)
|
||||
addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2)
|
||||
end
|
||||
end
|
||||
|
||||
socks = []
|
||||
addr_by_socket = {}
|
||||
|
||||
addresses.each do |address|
|
||||
check_private_address(address, host)
|
||||
|
||||
sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
|
||||
sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
|
||||
|
||||
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
|
||||
|
||||
sock.connect_nonblock(sockaddr)
|
||||
|
||||
# If that hasn't raised an exception, we somehow managed to connect
|
||||
# immediately, close pending sockets and return immediately
|
||||
socks.each(&:close)
|
||||
return sock
|
||||
rescue IO::WaitWritable
|
||||
socks << sock
|
||||
addr_by_socket[sock] = sockaddr
|
||||
rescue => e
|
||||
outer_e = e
|
||||
end
|
||||
|
||||
until socks.empty?
|
||||
_, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect_timeout])
|
||||
|
||||
if available_socks.nil?
|
||||
socks.each(&:close)
|
||||
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect_timeout]} seconds"
|
||||
end
|
||||
|
||||
available_socks.each do |sock|
|
||||
socks.delete(sock)
|
||||
|
||||
begin
|
||||
sock.connect_nonblock(addr_by_socket[sock])
|
||||
rescue Errno::EISCONN
|
||||
# Do nothing
|
||||
rescue => e
|
||||
sock.close
|
||||
outer_e = e
|
||||
next
|
||||
end
|
||||
|
||||
socks.each(&:close)
|
||||
return sock
|
||||
end
|
||||
end
|
||||
|
||||
if outer_e
|
||||
raise outer_e
|
||||
else
|
||||
raise SocketError, "No address for #{host}"
|
||||
end
|
||||
end
|
||||
|
||||
alias new open
|
||||
|
||||
def check_private_address(address, host)
|
||||
addr = IPAddr.new(address.to_s)
|
||||
|
||||
return if Rails.env.development? || private_address_exceptions.any? { |range| range.include?(addr) }
|
||||
|
||||
raise Mastodon::PrivateNetworkAddressError, host if PrivateAddressCheck.private_address?(addr)
|
||||
end
|
||||
|
||||
def private_address_exceptions
|
||||
@private_address_exceptions = (ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(/(?:\s*,\s*|\s+)/).map { |addr| IPAddr.new(addr) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ProxySocket < Socket
|
||||
class << self
|
||||
def check_private_address(_address, _host)
|
||||
# Accept connections to private addresses as HTTP proxies will usually
|
||||
# be on local addresses
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private_constant :ClientLimit, :Socket, :ProxySocket
|
||||
end
|
||||
@@ -0,0 +1,174 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TextFormatter
|
||||
include ActionView::Helpers::TextHelper
|
||||
include ERB::Util
|
||||
include RoutingHelper
|
||||
|
||||
URL_PREFIX_REGEX = %r{\A(https?://(www\.)?|xmpp:)}
|
||||
|
||||
DEFAULT_REL = %w(nofollow noopener noreferrer).freeze
|
||||
|
||||
DEFAULT_OPTIONS = {
|
||||
multiline: true,
|
||||
}.freeze
|
||||
|
||||
attr_reader :text, :options
|
||||
|
||||
# @param [String] text
|
||||
# @param [Hash] options
|
||||
# @option options [Boolean] :multiline
|
||||
# @option options [Boolean] :with_domains
|
||||
# @option options [Boolean] :with_rel_me
|
||||
# @option options [Array<Account>] :preloaded_accounts
|
||||
def initialize(text, options = {})
|
||||
@text = text
|
||||
@options = DEFAULT_OPTIONS.merge(options)
|
||||
end
|
||||
|
||||
def entities
|
||||
@entities ||= Extractor.extract_entities_with_indices(text, extract_url_without_protocol: false)
|
||||
end
|
||||
|
||||
def to_s
|
||||
return ''.html_safe if text.blank?
|
||||
|
||||
html = rewrite do |entity|
|
||||
if entity[:url]
|
||||
link_to_url(entity)
|
||||
elsif entity[:hashtag]
|
||||
link_to_hashtag(entity)
|
||||
elsif entity[:screen_name]
|
||||
link_to_mention(entity)
|
||||
end
|
||||
end
|
||||
|
||||
html = simple_format(html, {}, sanitize: false).delete("\n") if multiline?
|
||||
|
||||
html.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
|
||||
class << self
|
||||
include ERB::Util
|
||||
|
||||
def shortened_link(url, rel_me: false)
|
||||
url = Addressable::URI.parse(url).to_s
|
||||
rel = rel_me ? (DEFAULT_REL + %w(me)) : DEFAULT_REL
|
||||
|
||||
prefix = url.match(URL_PREFIX_REGEX).to_s
|
||||
display_url = url[prefix.length, 30]
|
||||
<<<<<<< HEAD
|
||||
suffix = url[prefix.length + 30..]
|
||||
cutoff = url[prefix.length..].length > 30
|
||||
|
||||
<<~HTML.squish.html_safe # rubocop:disable Rails/OutputSafety
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}" translate="no"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
=======
|
||||
suffix = url[prefix.length + 30..-1]
|
||||
cutoff = url[prefix.length..-1].length > 30
|
||||
|
||||
<<~HTML.squish
|
||||
<a href="#{h(url)}" target="_blank" rel="#{rel.join(' ')}"><span class="invisible">#{h(prefix)}</span><span class="#{cutoff ? 'ellipsis' : ''}">#{h(display_url)}</span><span class="invisible">#{h(suffix)}</span></a>
|
||||
>>>>>>> 32ebeed59 (Merge pull request from GHSA-55j9-c3mp-6fcq)
|
||||
HTML
|
||||
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
|
||||
h(url)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def rewrite
|
||||
entities.sort_by! do |entity|
|
||||
entity[:indices].first
|
||||
end
|
||||
|
||||
result = +''
|
||||
|
||||
last_index = entities.reduce(0) do |index, entity|
|
||||
indices = entity[:indices]
|
||||
result << h(text[index...indices.first])
|
||||
result << yield(entity)
|
||||
indices.last
|
||||
end
|
||||
|
||||
result << h(text[last_index..])
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
def link_to_url(entity)
|
||||
TextFormatter.shortened_link(entity[:url], rel_me: with_rel_me?)
|
||||
end
|
||||
|
||||
def link_to_hashtag(entity)
|
||||
hashtag = entity[:hashtag]
|
||||
url = tag_url(hashtag)
|
||||
|
||||
<<~HTML.squish
|
||||
<a href="#{h(url)}" class="mention hashtag" rel="tag">#<span>#{h(hashtag)}</span></a>
|
||||
HTML
|
||||
end
|
||||
|
||||
def link_to_mention(entity)
|
||||
username, domain = entity[:screen_name].split('@')
|
||||
domain = nil if local_domain?(domain)
|
||||
account = nil
|
||||
|
||||
if preloaded_accounts?
|
||||
same_username_hits = 0
|
||||
|
||||
preloaded_accounts.each do |other_account|
|
||||
same_username = other_account.username.casecmp(username).zero?
|
||||
same_domain = other_account.domain.nil? ? domain.nil? : other_account.domain.casecmp(domain)&.zero?
|
||||
|
||||
if same_username && !same_domain
|
||||
same_username_hits += 1
|
||||
elsif same_username && same_domain
|
||||
account = other_account
|
||||
end
|
||||
end
|
||||
else
|
||||
account = entity_cache.mention(username, domain)
|
||||
end
|
||||
|
||||
return "@#{h(entity[:screen_name])}" if account.nil?
|
||||
|
||||
url = ActivityPub::TagManager.instance.url_for(account)
|
||||
display_username = same_username_hits&.positive? || with_domains? ? account.pretty_acct : account.username
|
||||
|
||||
<<~HTML.squish
|
||||
<span class="h-card" translate="no"><a href="#{h(url)}" class="u-url mention">@<span>#{h(display_username)}</span></a></span>
|
||||
HTML
|
||||
end
|
||||
|
||||
def entity_cache
|
||||
@entity_cache ||= EntityCache.instance
|
||||
end
|
||||
|
||||
def tag_manager
|
||||
@tag_manager ||= TagManager.instance
|
||||
end
|
||||
|
||||
delegate :local_domain?, to: :tag_manager
|
||||
|
||||
def multiline?
|
||||
options[:multiline]
|
||||
end
|
||||
|
||||
def with_domains?
|
||||
options[:with_domains]
|
||||
end
|
||||
|
||||
def with_rel_me?
|
||||
options[:with_rel_me]
|
||||
end
|
||||
|
||||
def preloaded_accounts
|
||||
options[:preloaded_accounts]
|
||||
end
|
||||
|
||||
def preloaded_accounts?
|
||||
preloaded_accounts.present?
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user