From 5b54cd7f766dda19cbfdb02a1f94f45de1c18379 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Jan 2026 11:40:26 +0100 Subject: [PATCH 1/9] Add ability to include inline javascript (#37459) --- app/helpers/theme_helper.rb | 16 ++++++++++ app/javascript/inline/theme-selection.js | 23 ++++++++++++++ app/lib/inline_script_manager.rb | 31 +++++++++++++++++++ app/views/layouts/application.html.haml | 1 + spec/requests/content_security_policy_spec.rb | 2 +- 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 app/javascript/inline/theme-selection.js create mode 100644 app/lib/inline_script_manager.rb diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 00b4a6d2b3..1d64205680 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -1,6 +1,22 @@ # frozen_string_literal: true module ThemeHelper + def javascript_inline_tag(path) + entry = InlineScriptManager.instance.file(path) + + # Only add hash if we don't allow arbitrary includes already, otherwise it's going + # to break the React Tools browser extension or other inline scripts + unless Rails.env.development? && request.content_security_policy.dup.script_src.include?("'unsafe-inline'") + request.content_security_policy = request.content_security_policy.clone.tap do |policy| + values = policy.script_src + values << "'sha256-#{entry[:digest]}'" + policy.script_src(*values) + end + end + + content_tag(:script, entry[:contents], type: 'text/javascript') + end + def theme_style_tags(theme) if theme == 'system' ''.html_safe.tap do |tags| diff --git a/app/javascript/inline/theme-selection.js b/app/javascript/inline/theme-selection.js new file mode 100644 index 0000000000..b3a2b03163 --- /dev/null +++ b/app/javascript/inline/theme-selection.js @@ -0,0 +1,23 @@ +(function (element) { + const {userTheme} = element.dataset; + + const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)'); + const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)'); + + const updateColorScheme = () => { + const useDarkMode = userTheme === 'system' ? colorSchemeMediaWatcher.matches : userTheme !== 'mastodon-light'; + element.dataset.mode = useDarkMode ? 'dark' : 'light'; + }; + + const updateContrast = () => { + const useHighContrast = userTheme === 'contrast' || contrastMediaWatcher.matches; + + element.dataset.contrast = useHighContrast ? 'high' : 'default'; + } + + colorSchemeMediaWatcher.addEventListener('change', updateColorScheme); + contrastMediaWatcher.addEventListener('change', updateContrast); + + updateColorScheme(); + updateContrast(); +})(document.documentElement); diff --git a/app/lib/inline_script_manager.rb b/app/lib/inline_script_manager.rb new file mode 100644 index 0000000000..bca7c98f6b --- /dev/null +++ b/app/lib/inline_script_manager.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'singleton' + +class InlineScriptManager + include Singleton + include ActionView::Helpers::TagHelper + include ActionView::Helpers::JavaScriptHelper + + def initialize + @cached_files = {} + end + + def file(name) + @cached_files[name] ||= load_file(name) + end + + private + + def load_file(name) + path = Pathname.new(name).cleanpath + raise ArgumentError, "Invalid inline javascript path: #{path}" if path.to_s.start_with?('..') + + path = Rails.root.join('app', 'javascript', 'inline', path) + + contents = javascript_cdata_section(path.read) + digest = Digest::SHA256.base64digest(contents) + + { contents:, digest: } + end +end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 47e602f0f3..dccb6035ca 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -20,6 +20,7 @@ - if use_mask_icon? %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ + = javascript_inline_tag 'theme-selection.js' = theme_color_tags current_theme %meta{ name: 'mobile-web-app-capable', content: 'yes' }/ diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb index 0a58a03ffa..0aa4494ef0 100644 --- a/spec/requests/content_security_policy_spec.rb +++ b/spec/requests/content_security_policy_spec.rb @@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do img-src 'self' data: blob: #{local_domain} manifest-src 'self' #{local_domain} media-src 'self' data: #{local_domain} - script-src 'self' #{local_domain} 'wasm-unsafe-eval' + script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Q/2Cjx8v06hAdOF8/DeBUpsmBcSj7sLN3I/WpTF8T8c=' style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ==' worker-src 'self' blob: #{local_domain} CSP From be60c4585ebdac0cc236fb597246922af3fb6ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Tue, 13 Jan 2026 05:47:04 -0500 Subject: [PATCH 2/9] Fix keyboard navigation in media modal after clicking image (#37464) --- app/javascript/mastodon/features/ui/components/media_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/media_modal.tsx b/app/javascript/mastodon/features/ui/components/media_modal.tsx index ac762aa18d..2510137682 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.tsx @@ -85,7 +85,7 @@ export const MediaModal: FC = forwardRef< setIndex(newIndex); setZoomedIn(false); if (animate) { - void api.start({ x: `-${newIndex * 100}%` }); + void api.start({ x: `calc(-${newIndex * 100}% + 0px)` }); } }, [api, media.size], From 41639655ffaa9914632fe937c9583b901b36f5cc Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 13 Jan 2026 12:06:54 +0100 Subject: [PATCH 3/9] Fix `isDarkMode` utility (#37470) --- app/javascript/mastodon/utils/theme.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/utils/theme.ts b/app/javascript/mastodon/utils/theme.ts index 767d97cf5c..921787a6c4 100644 --- a/app/javascript/mastodon/utils/theme.ts +++ b/app/javascript/mastodon/utils/theme.ts @@ -5,9 +5,7 @@ export function getUserTheme() { export function isDarkMode() { const { userTheme } = document.documentElement.dataset; - return ( - (userTheme === 'system' && - window.matchMedia('(prefers-color-scheme: dark)').matches) || - userTheme !== 'mastodon-light' - ); + return userTheme === 'system' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + : userTheme !== 'mastodon-light'; } From d5264b37224632682c15b5e565fcc5fee3ae4834 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:32:11 +0100 Subject: [PATCH 4/9] Update dependency aws-sdk-s3 to v1.211.0 (#37396) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 57f4f91b35..2167d01739 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,8 +96,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1200.0) - aws-sdk-core (3.240.0) + aws-partitions (1.1201.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -105,11 +105,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.209.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) From 232b9e9cc60506822bd6121b5dcd9707b1ea8c24 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 20:49:36 +0700 Subject: [PATCH 5/9] Fix delivery worker counting unsalvageable HTTP errors as successes (#37235) --- app/workers/activitypub/delivery_worker.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index ade7175c9d..8cd39f700c 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -38,7 +38,7 @@ class ActivityPub::DeliveryWorker if @inbox_url.present? if @performed failure_tracker.track_success! - else + elsif !@unsalvageable failure_tracker.track_failure! end end @@ -62,9 +62,13 @@ class ActivityPub::DeliveryWorker stoplight_wrapper.run do request_pool.with(@host) do |http_client| build_request(http_client).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) - - @performed = true + if response_successful?(response) + @performed = true + elsif response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) + @unsalvageable = true + else + raise Mastodon::UnexpectedResponseError, response + end end end end From 122b1592ed543838f2047ef82e570ca58a087b32 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 13 Jan 2026 15:17:43 +0100 Subject: [PATCH 6/9] Add feature flag detection for profile redesign (#37472) --- app/javascript/mastodon/utils/environment.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 95075454f2..84767322b0 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,8 +12,21 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'fasp' | 'http_message_signatures'; +export type ServerFeatures = 'fasp'; -export function isFeatureEnabled(feature: Features) { +export function isServerFeatureEnabled(feature: ServerFeatures) { return initialState?.features.includes(feature) ?? false; } + +type ClientFeatures = 'profile_redesign'; + +export function isClientFeatureEnabled(feature: ClientFeatures) { + try { + const features = + window.localStorage.getItem('experiments')?.split(',') ?? []; + return features.includes(feature); + } catch (err) { + console.warn('Could not access localStorage to get client features', err); + return false; + } +} From c6be114cef98b219869cd3181d4eda9243dce5c6 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 22:47:48 +0700 Subject: [PATCH 7/9] Non-ActivityPub Link header alternate blocks HTML ActivityPub discovery in FetchResourceService (#37439) Co-authored-by: Claire --- app/services/fetch_resource_service.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 666eccb9ce..514c838d64 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -58,13 +58,7 @@ class FetchResourceService < BaseService [@url, { prefetched_body: body }] elsif !terminal - link_header = response['Link'] && parse_link_header(response) - - if link_header&.find_link(%w(rel alternate)) - process_link_headers(link_header) - elsif response.mime_type == 'text/html' - process_html(response) - end + process_link_headers(response) || process_html(response) end end @@ -73,13 +67,18 @@ class FetchResourceService < BaseService end def process_html(response) + return unless response.mime_type == 'text/html' + page = Nokogiri::HTML5(response.body_with_limit) json_link = page.xpath('//link[nokogiri:link_rel_include(@rel, "alternate")]', NokogiriHandler).find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) } process(json_link['href'], terminal: true) unless json_link.nil? end - def process_link_headers(link_header) + def process_link_headers(response) + link_header = response['Link'] && parse_link_header(response) + return if link_header.nil? + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) process(json_link.href, terminal: true) unless json_link.nil? From 92ad380e118ebf12db344786f6862b1eb64365d0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 13 Jan 2026 11:21:25 -0500 Subject: [PATCH 8/9] Update rubocop to version 1.82.1 (#37475) --- Gemfile.lock | 6 ++--- app/workers/move_worker.rb | 36 ++++++++++++++------------ lib/paperclip/attachment_extensions.rb | 3 +-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2167d01739..e7519ea90b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -752,7 +752,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.6) - rubocop (1.81.7) + rubocop (1.82.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -760,7 +760,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -782,7 +782,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.8.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) rubocop (~> 1.81) rubocop-rspec_rails (2.32.0) diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 1a5745a86a..43238b72b2 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -45,30 +45,34 @@ class MoveWorker end # Then handle accounts that follow both the old and new account - @source_account.passive_relationships - .where(account: Account.local) - .where(account: @target_account.followers.local) - .in_batches do |follows| - ListAccount.where(follow: follows).includes(:list).find_each do |list_account| - list_account.list.accounts << @target_account - rescue ActiveRecord::RecordInvalid - nil - end + source_local_followers + .where(account: @target_account.followers.local) + .in_batches do |follows| + ListAccount.where(follow: follows).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end end # Finally, handle the common case of accounts not following the new account - @source_account.passive_relationships - .where(account: Account.local) - .where.not(account: @target_account.followers.local) - .where.not(account_id: @target_account.id) - .in_batches do |follows| - ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) - num_moved += follows.update_all(target_account_id: @target_account.id) + source_local_followers + .where.not(account: @target_account.followers.local) + .where.not(account_id: @target_account.id) + .in_batches do |follows| + ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) + num_moved += follows.update_all(target_account_id: @target_account.id) end num_moved end + def source_local_followers + @source_account + .passive_relationships + .where(account: Account.local) + end + def queue_follow_unfollows! bypass_locked = @target_account.local? diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 011e165ed7..7141adc9ed 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -16,8 +16,7 @@ module Paperclip # if we're processing the original, close + unlink the source tempfile intermediate_files << original if name == :original - @queued_for_write[name] = style.processors - .inject(original) do |file, processor| + @queued_for_write[name] = style.processors.inject(original) do |file, processor| file = Paperclip.processor(processor).make(file, style.processor_options, self) intermediate_files << file unless file == original file From 19bc3e76ea1d7fa2d9af711613a77fc0f5f0d1b5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 13 Jan 2026 11:21:55 -0500 Subject: [PATCH 9/9] Add spec for quote policy update change (#37474) --- spec/requests/api/v1/statuses_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 5db9889e2d..3fbf26c54a 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') end end + + context 'when status has non-default quote policy and param is omitted' do + let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') } + + it 'preserves existing quote approval policy' do + expect { subject } + .to_not(change { status.reload.quote_approval_policy }) + end + end end end