From c0264c8013615cb725f9fd2d69bea60ea4a13324 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 8 Jan 2025 03:48:45 -0500 Subject: [PATCH] Extend custom CSS cache time with digest paths (#33207) --- app/controllers/custom_css_controller.rb | 2 +- app/helpers/theme_helper.rb | 23 +++++++++++++++ app/models/form/admin_settings.rb | 18 ++++++++++++ app/views/layouts/application.html.haml | 2 +- config/initializers/settings_digests.rb | 18 ++++++++++++ config/routes.rb | 3 +- spec/helpers/theme_helper_spec.rb | 20 +++++++++++++ spec/models/form/admin_settings_spec.rb | 36 ++++++++++++++++++++++++ spec/requests/cache_spec.rb | 1 + spec/requests/custom_css_spec.rb | 6 ++-- spec/routing/custom_css_routing_spec.rb | 19 +++++++++++++ 11 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 config/initializers/settings_digests.rb create mode 100644 spec/routing/custom_css_routing_spec.rb diff --git a/app/controllers/custom_css_controller.rb b/app/controllers/custom_css_controller.rb index 9f33870bd2..5b98914114 100644 --- a/app/controllers/custom_css_controller.rb +++ b/app/controllers/custom_css_controller.rb @@ -2,7 +2,7 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController def show - expires_in 3.minutes, public: true + expires_in 1.month, public: true render content_type: 'text/css' end diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index fab899a533..5dfb4a5184 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -23,8 +23,31 @@ module ThemeHelper end end + def custom_stylesheet + if active_custom_stylesheet.present? + stylesheet_link_tag( + custom_css_path(active_custom_stylesheet), + host: root_url, + media: :all, + skip_pipeline: true + ) + end + end + private + def active_custom_stylesheet + if cached_custom_css_digest.present? + [:custom, cached_custom_css_digest.to_s.first(8)] + .compact_blank + .join('-') + end + end + + def cached_custom_css_digest + Rails.cache.read(:setting_digest_custom_css) + end + def theme_color_for(theme) theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] end diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index 515909d5cf..ffd2d4049c 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -69,6 +69,10 @@ class Form::AdminSettings favicon ).freeze + DIGEST_KEYS = %i( + custom_css + ).freeze + OVERRIDEN_SETTINGS = { authorized_fetch: :authorized_fetch_mode?, }.freeze @@ -122,6 +126,8 @@ class Form::AdminSettings KEYS.each do |key| next unless instance_variable_defined?(:"@#{key}") + cache_digest_value(key) if DIGEST_KEYS.include?(key) + if UPLOAD_KEYS.include?(key) public_send(key).save else @@ -133,6 +139,18 @@ class Form::AdminSettings private + def cache_digest_value(key) + Rails.cache.delete(:"setting_digest_#{key}") + + key_value = instance_variable_get(:"@#{key}") + if key_value.present? + Rails.cache.write( + :"setting_digest_#{key}", + Digest::SHA256.hexdigest(key_value) + ) + end + end + def typecast_value(key, value) if BOOLEAN_KEYS.include?(key) value == '1' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 99e89d45ce..6f016c6cf5 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -34,7 +34,7 @@ = csrf_meta_tags unless skip_csrf_meta_tags? %meta{ name: 'style-nonce', content: request.content_security_policy_nonce } - = stylesheet_link_tag custom_css_path, skip_pipeline: true, host: root_url, media: 'all' + = custom_stylesheet = yield :header_tags diff --git a/config/initializers/settings_digests.rb b/config/initializers/settings_digests.rb new file mode 100644 index 0000000000..2a5d925c70 --- /dev/null +++ b/config/initializers/settings_digests.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +Rails.application.config.to_prepare do + custom_css = begin + Setting.custom_css + rescue ActiveRecord::AdapterError # Running without a database, not migrated, no connection, etc + nil + end + + if custom_css.present? + Rails + .cache + .write( + :setting_digest_custom_css, + Digest::SHA256.hexdigest(custom_css) + ) + end +end diff --git a/config/routes.rb b/config/routes.rb index 3909dd1b77..5adec04c7d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,7 +55,8 @@ Rails.application.routes.draw do get 'manifest', to: 'manifests#show', defaults: { format: 'json' } get 'intent', to: 'intents#show' - get 'custom.css', to: 'custom_css#show', as: :custom_css + get 'custom.css', to: 'custom_css#show' + resources :custom_css, only: :show, path: :css get 'remote_interaction_helper', to: 'remote_interaction_helper#index' diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 7663e59436..c811b7c981 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -79,6 +79,26 @@ RSpec.describe ThemeHelper do end end + describe '#custom_stylesheet' do + context 'when custom css setting value digest is present' do + before { Rails.cache.write(:setting_digest_custom_css, '1a2s3d4f1a2s3d4f') } + + it 'returns value from settings' do + expect(custom_stylesheet) + .to match('/css/custom-1a2s3d4f.css') + end + end + + context 'when custom css setting value digest is not present' do + before { Rails.cache.delete(:setting_digest_custom_css) } + + it 'returns default value' do + expect(custom_stylesheet) + .to be_blank + end + end + end + private def html_links diff --git a/spec/models/form/admin_settings_spec.rb b/spec/models/form/admin_settings_spec.rb index 73106f2b69..899d56703a 100644 --- a/spec/models/form/admin_settings_spec.rb +++ b/spec/models/form/admin_settings_spec.rb @@ -17,4 +17,40 @@ RSpec.describe Form::AdminSettings do end end end + + describe '#save' do + describe 'updating digest values' do + context 'when updating custom css to real value' do + subject { described_class.new(custom_css: css) } + + let(:css) { 'body { color: red; }' } + let(:digested) { Digest::SHA256.hexdigest(css) } + + it 'changes relevant digest value' do + expect { subject.save } + .to(change { Rails.cache.read(:setting_digest_custom_css) }.to(digested)) + end + end + + context 'when updating custom css to empty value' do + subject { described_class.new(custom_css: '') } + + before { Rails.cache.write(:setting_digest_custom_css, 'previous-value') } + + it 'changes relevant digest value' do + expect { subject.save } + .to(change { Rails.cache.read(:setting_digest_custom_css) }.to(be_blank)) + end + end + + context 'when updating other fields' do + subject { described_class.new(site_contact_email: 'test@example.host') } + + it 'does not update digests' do + expect { subject.save } + .to(not_change { Rails.cache.read(:setting_digest_custom_css) }) + end + end + end + end end diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb index 2a52e4dea9..8ca2817263 100644 --- a/spec/requests/cache_spec.rb +++ b/spec/requests/cache_spec.rb @@ -10,6 +10,7 @@ module TestEndpoints /.well-known/nodeinfo /nodeinfo/2.0 /manifest + /css/custom-1a2s3d4f.css /custom.css /actor /api/v1/instance/extended_description diff --git a/spec/requests/custom_css_spec.rb b/spec/requests/custom_css_spec.rb index 380c32908a..66ff5c4b13 100644 --- a/spec/requests/custom_css_spec.rb +++ b/spec/requests/custom_css_spec.rb @@ -5,10 +5,10 @@ require 'rails_helper' RSpec.describe 'Custom CSS' do include RoutingHelper - describe 'GET /custom.css' do + describe 'GET /css/:id.css' do context 'without any CSS or User Roles' do it 'returns empty stylesheet' do - get '/custom.css' + get '/css/custom-123.css' expect(response) .to have_http_status(200) @@ -27,7 +27,7 @@ RSpec.describe 'Custom CSS' do end it 'returns stylesheet from settings' do - get '/custom.css' + get '/css/custom-456.css' expect(response) .to have_http_status(200) diff --git a/spec/routing/custom_css_routing_spec.rb b/spec/routing/custom_css_routing_spec.rb new file mode 100644 index 0000000000..26139b4744 --- /dev/null +++ b/spec/routing/custom_css_routing_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Custom CSS routes' do + describe 'the legacy route' do + it 'routes to correct place' do + expect(get('/custom.css')) + .to route_to('custom_css#show') + end + end + + describe 'the custom digest route' do + it 'routes to correct place' do + expect(get('/css/custom-1a2s3d4f.css')) + .to route_to('custom_css#show', id: 'custom-1a2s3d4f', format: 'css') + end + end +end