Add: #406 ユーザーのカスタムCSS (#825)

* Add: #406 ユーザーのカスタムCSS

* Fix lint

* Fix lint

* カスタムCSSの保存先を変更

* キャッシュを考慮して別URLに変更
This commit is contained in:
KMY(雪あすか) 2024-08-29 07:55:01 +09:00 committed by GitHub
parent 5ffd7593f1
commit 665c632d66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 283 additions and 4 deletions

View file

@ -78,7 +78,11 @@ class Auth::SessionsController < Devise::SessionsController
end
def user_params
params.require(:user).permit(:email, :password, :otp_attempt, credential: {})
params.require(:user).permit(:email, :password, :otp_attempt, :disable_css, credential: {})
end
def login_page_params
params.permit(:with_options)
end
def after_sign_in_path_for(resource)
@ -113,6 +117,11 @@ class Auth::SessionsController < Devise::SessionsController
truthy_param?(:continue)
end
def with_login_options?
login_page_params[:with_options] == '1'
end
helper_method :with_login_options?
def restart_session
clear_attempt_from_session
redirect_to new_user_session_path, alert: I18n.t('devise.failure.timeout')
@ -151,6 +160,8 @@ class Auth::SessionsController < Devise::SessionsController
sign_in(user)
flash.delete(:notice)
disable_custom_css!(user) if disable_custom_css?
LoginActivity.create(
user: user,
success: true,
@ -162,6 +173,15 @@ class Auth::SessionsController < Devise::SessionsController
UserMailer.suspicious_sign_in(user, request.remote_ip, request.user_agent, Time.now.utc).deliver_later! if @login_is_suspicious
end
def disable_custom_css?
user_params[:disable_css].present? && user_params[:disable_css] != '0'
end
def disable_custom_css!(user)
user.settings['web.use_custom_css'] = false
user.save!
end
def suspicious_sign_in?(user)
SuspiciousSignInDetector.new(user).suspicious?(request)
end

View file

@ -13,7 +13,17 @@ class CustomCssController < ActionController::Base # rubocop:disable Rails/Appli
def custom_css_styles
Setting.custom_css
end
helper_method :custom_css_styles
def user_custom_css?
return false if current_user.nil?
current_user.setting_use_custom_css && current_user.custom_css_text.present?
end
def user_custom_css
current_user.custom_css_text
end
helper_method :custom_css_styles, :user_custom_css?, :user_custom_css
def set_user_roles
@user_roles = UserRole.providing_styles

View file

@ -25,7 +25,7 @@ class Settings::Preferences::BaseController < Settings::BaseController
end
def original_user_params
params.require(:user).permit(:locale, :time_zone, chosen_languages: [], settings_attributes: UserSettings.keys)
params.require(:user).permit(:locale, :time_zone, :custom_css_text, chosen_languages: [], settings_attributes: UserSettings.keys)
end
def disabled_visibilities_params

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Settings::Preferences::CustomCssController < Settings::Preferences::BaseController
private
def after_update_redirect_path
settings_preferences_custom_css_path
end
end

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
class UserCustomCssController < ActionController::Base # rubocop:disable Rails/ApplicationController
before_action :authenticate_user!
def show
render content_type: 'text/css'
end
private
def user_custom_css
current_user.custom_css_text
end
helper_method :user_custom_css
end

View file

@ -252,6 +252,18 @@ module ApplicationHelper
full_asset_url(instance_presenter.mascot&.file&.url || frontend_asset_path('images/elephant_ui_plane.svg'))
end
def user_custom_css?
return false if current_account&.user.nil?
current_account.user.setting_use_custom_css && current_account.user.custom_css_text.present?
end
def user_custom_css_version
return '0' if current_account&.user&.custom_css.nil?
current_account&.user&.custom_css&.updated_at.to_s
end
private
def storage_host_var

View file

@ -283,6 +283,10 @@ module User::HasSettings
settings['web.hide_favourite_menu']
end
def setting_use_custom_css
settings['web.use_custom_css']
end
def allows_report_emails?
settings['notification_emails.report']
end

16
app/models/custom_css.rb Normal file
View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
#
# == Schema Information
#
# Table name: custom_csses
#
# id :bigint(8) not null, primary key
# user_id :bigint(8) not null
# css :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class CustomCss < ApplicationRecord
belongs_to :user
end

View file

@ -101,6 +101,8 @@ class User < ApplicationRecord
accepts_nested_attributes_for :invite_request, reject_if: ->(attributes) { attributes['text'].blank? && !Setting.require_invite_text }
validates :invite_request, presence: true, on: :create, if: :invite_text_required?
has_one :custom_css, inverse_of: :user, dependent: :destroy
validates :email, presence: true, email_address: true
validates_with UserEmailValidator, if: -> { ENV['EMAIL_DOMAIN_LISTS_APPLY_AFTER_CONFIRMATION'] == 'true' || !confirmed? }
@ -227,6 +229,22 @@ class User < ApplicationRecord
prepare_returning_user!
end
def disable_css
false
end
def custom_css_text
custom_css&.css.to_s
end
def custom_css_text=(val)
if custom_css.present?
custom_css.update!(css: val)
else
CustomCss.create!(user: self, css: val)
end
end
def pending?
!approved?
end

View file

@ -55,6 +55,7 @@ class UserSettings
setting :use_blurhash, default: true
setting :use_pending_items, default: false
setting :use_system_font, default: false
setting :use_custom_css, default: false
setting :content_font_size, default: 'medium', in: %w(medium large x_large xx_large)
setting :bookmark_category_needed, default: false
setting :disable_swiping, default: false

View file

@ -63,6 +63,8 @@ class UserSettings::Setting
case default_value
when TrueClass, FalseClass
ActiveModel::Type::Boolean.new
when Integer
ActiveModel::Type::Integer.new
else
ActiveModel::Type::String.new
end

View file

@ -30,6 +30,15 @@
label: t('simple_form.labels.defaults.password'),
wrapper: :with_label
- if with_login_options?
.fields-group
= f.input :disable_css,
as: :boolean,
hint: false,
input_html: { 'aria-label': t('auth.disable_custom_css') },
label: t('auth.disable_custom_css'),
wrapper: :with_label
.actions
= f.button :button, t('auth.login'), type: :submit

View file

@ -11,6 +11,9 @@
- if controller_name != 'passwords' && controller_name != 'registrations'
%li= link_to t('auth.forgot_password'), new_user_password_path
- if controller_name != 'passwords' && controller_name != 'registrations' && params[:with_options].nil?
%li= link_to t('auth.with_login_options'), new_user_session_path(with_options: '1')
- if controller_name != 'confirmations' && (!user_signed_in? || !current_user.confirmed? || current_user.unconfirmed_email.present?)
%li= link_to t('auth.didnt_get_confirmation'), new_user_confirmation_path

View file

@ -36,6 +36,9 @@
= stylesheet_link_tag custom_css_path, skip_pipeline: true, host: root_url, media: 'all'
- if user_custom_css?
= stylesheet_link_tag user_custom_css_path({ version: user_custom_css_version }), skip_pipeline: true, host: root_url, media: 'all'
= yield :header_tags
%body{ class: body_classes }

View file

@ -0,0 +1,30 @@
- content_for :page_title do
= t('simple_form.labels.form_admin_settings.custom_css')
- content_for :heading_actions do
= button_tag t('generic.save_changes'), class: 'button', form: 'edit_preferences'
= simple_form_for current_user, url: settings_preferences_custom_css_path, html: { method: :put, id: 'edit_preferences' } do |f|
= render 'shared/error_messages', object: current_user
= f.simple_fields_for :settings, current_user.settings do |ff|
.fields-group
= ff.input :'web.use_custom_css',
hint: false,
label: I18n.t('simple_form.labels.defaults.setting_use_custom_css'),
kmyblue: true,
wrapper: :with_label
.fields-group
= f.input :custom_css_text,
as: :text,
hint: false,
input_html: { rows: 12 },
label: I18n.t('simple_form.labels.defaults.setting_custom_css'),
kmyblue: true,
wrapper: :with_label
%p.hint= t 'simple_form.hints.defaults.setting_custom_css_lead'
.actions
= f.button :button, t('generic.save_changes'), type: :submit

View file

@ -0,0 +1 @@
<%= raw user_custom_css %>

View file

@ -1418,6 +1418,7 @@ en:
prefix_sign_up: Sign up on Mastodon today!
suffix: With an account, you will be able to follow people, post updates and exchange messages with users from any Mastodon server and more!
didnt_get_confirmation: Didn't receive a confirmation link?
disable_custom_css: Disable custom CSS
dont_have_your_security_key: Don't have your security key?
forgot_password: Forgot your password?
invalid_reset_password_token: Password reset token is invalid or expired. Please request a new one.
@ -1478,6 +1479,7 @@ en:
view_strikes: View past strikes against your account
too_fast: Form submitted too fast, try again.
use_security_key: Use security key
with_login_options: Will you disable your custom css?
bookmark_categories:
errors:
limit: Bookmark category limit
@ -1906,6 +1908,7 @@ en:
too_few_options: must have more than one item
too_many_options: can't contain more than %{max} items
preferences:
custom_css: Custom css
does_not_search: The full-text search feature is not available on this server. Instead, your posts will be searched according to this setting on other kmyblue servers.
dtl: Deep timeline
dtl_hint: 'You can join deep timeline with #%{tag} tag. Following settings make convenient to use deep timeline.'

View file

@ -1329,6 +1329,7 @@ ja:
prefix_sign_up: 今すぐMastodonを始めよう
suffix: アカウントがあれば、どんなMastodon互換サーバーのユーザーでもフォローしたりメッセージをやり取りできるようになります
didnt_get_confirmation: 確認メールを受信できない場合は
disable_custom_css: カスタムCSSを無効化する
dont_have_your_security_key: セキュリティキーを持っていませんか?
forgot_password: パスワードをお忘れですか?
invalid_reset_password_token: パスワードリセットトークンが正しくないか期限切れです。もう一度リクエストしてください。
@ -1384,6 +1385,7 @@ ja:
view_strikes: 過去のストライクを表示
too_fast: フォームの送信が速すぎます。もう一度やり直してください。
use_security_key: セキュリティキーを使用
with_login_options: カスタムCSSを無効化しますか
challenge:
confirm: 続ける
hint_html: 以後1時間はパスワードの再入力を求めません
@ -1782,6 +1784,7 @@ ja:
too_few_options: は複数必要です
too_many_options: は%{max}個までです
preferences:
custom_css: カスタムCSS
does_not_search: このサーバーでは全文検索機能を利用できません。代わりに、他のkmyblueサーバーであなたの投稿がこの設定に従って検索されます。
dtl: ディープタイムライン
dtl_hint: "#%{tag} ハッシュタグに参加することで、ディープタイムラインに投稿できます。ここではディープタイムラインを利用しやすくするための設定ができます。"

View file

@ -62,6 +62,7 @@ en:
setting_allow_quote: Subdued quotes are allowed regardless of this setting; you can quote freely from any source except kmyblue!
setting_always_send_emails: Normally e-mail notifications won't be sent when you are actively using Mastodon
setting_bookmark_category_needed: When removing from all category, unbookmarked automatically
setting_custom_css_lead: 'Be sure to remember: In the unlikely event that you make a mistake in entering your custom CSS and the screen does not display properly, you can disable your custom CSS from the link at the bottom of the sign-in screen. Open the sign-in screen in private mode of your browser, for example, and disable it.'
setting_default_searchability: On kmyblue and Fedibird, the search is based on the search permission setting; on Misskey, all public, local public, and non-public posts are searched regardless of this setting; on Mastodon and Firefish, instead of search permission, the "Make public posts freely searchable on other servers" setting in the profile settings is applied. In Mastodon and Firefish, the "Make public posts freely searchable on other servers" setting in the profile settings is applied instead of the search permission.
setting_default_sensitive: Sensitive media is hidden by default and can be revealed with a click
setting_disallow_unlisted_public_searchability: この設定を有効にすると、非収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります
@ -263,6 +264,7 @@ en:
medium: Default
x_large: Large large
xx_large: Large large large
setting_custom_css: Custom CSS
setting_default_language: Posting language
setting_default_privacy: Posting privacy
setting_default_reblog_privacy: Reblogging privacy
@ -325,6 +327,7 @@ en:
setting_trends: Show today's trends
setting_unfollow_modal: Show confirmation dialog before unfollowing someone
setting_use_blurhash: Show colorful gradients for hidden media
setting_use_custom_css: Enable custom CSS
setting_use_pending_items: Slow mode
setting_use_public_index: Include permitted accounts post to results of search
severity: Severity

View file

@ -62,6 +62,7 @@ ja:
setting_allow_quote: ひかえめな引用はこの設定に関わらず可能です。kmyblue以外からは自由に引用できます
setting_always_send_emails: 通常、Mastodon からメール通知は行われません。
setting_bookmark_category_needed: すべてのカテゴリから削除したとき、ブックマークが自動で外れるようになります
setting_custom_css_lead: '必ず覚えてください: 万が一カスタムCSSの入力を誤り、画面が正常に表示されなくなった場合は、サインイン画面の下にあるリンクよりカスタムCSSを無効化することができます。ブラウザのプライベートモードなどでサインイン画面を開き、無効化してください。'
setting_default_searchability: kmyblue・Fedibirdでは検索許可設定に基づき検索されます。Misskeyでは当設定に関係なく、全ての公開・ローカル公開・非収載投稿が検索されます。Mastodon・Firefishでは検索許可の代わりにプロフィール設定の「公開投稿を他のサーバーで自由に検索できるようにする」設定が適用されます
setting_default_sensitive: 閲覧注意状態のメディアはデフォルトでは内容が伏せられ、クリックして初めて閲覧できるようになります
setting_disallow_unlisted_public_searchability: この設定を有効にすると、非収載投稿と検索範囲「誰でも」は両立できず不特定多数からの検索が不可になります。Fedibirdと同じ挙動になります
@ -263,6 +264,7 @@ ja:
medium: デフォルト
x_large: 大きい大きい
xx_large: 大きい大きい大きい
setting_custom_css: カスタムCSS
setting_default_language: 投稿する言語
setting_default_privacy: 投稿の公開範囲
setting_default_reblog_privacy: BTの公開範囲
@ -325,6 +327,7 @@ ja:
setting_trends: 本日のトレンドタグを表示する
setting_unfollow_modal: フォローを解除する前に確認ダイアログを表示する
setting_use_blurhash: 非表示のメディアを色付きのぼかしで表示する
setting_use_custom_css: カスタムCSSを有効にする
setting_use_pending_items: 手動更新モード
setting_use_public_index: Mastodonの標準設定によって検索が許可されたアカウントの公開投稿を検索結果に含める
severity: 重大性

View file

@ -18,6 +18,7 @@ SimpleNavigation::Configuration.run do |navigation|
s.item :appearance, safe_join([material_symbol('computer'), t('settings.appearance')]), settings_preferences_appearance_path
s.item :notifications, safe_join([material_symbol('mail'), t('settings.notifications')]), settings_preferences_notifications_path
s.item :reaching, safe_join([material_symbol('search'), t('preferences.reaching')]), settings_preferences_reaching_path
s.item :custom_css, safe_join([material_symbol('inbox'), t('preferences.custom_css')]), settings_preferences_custom_css_path
s.item :other, safe_join([material_symbol('settings'), t('preferences.other')]), settings_preferences_other_path
end

View file

@ -87,6 +87,7 @@ 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 'user_custom.css', to: 'user_custom_css#show', as: :user_custom_css
get 'remote_interaction_helper', to: 'remote_interaction_helper#index'

View file

@ -11,6 +11,7 @@ namespace :settings do
resource :appearance, only: [:show, :update], controller: :appearance
resource :notifications, only: [:show, :update]
resource :reaching, only: [:show, :update], controller: :reaching
resource :custom_css, only: [:show, :update], controller: :custom_css
resource :other, only: [:show, :update], controller: :other
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateCustomCsses < ActiveRecord::Migration[7.1]
def change
create_table :custom_csses do |t|
t.belongs_to :user, foreign_key: { on_delete: :cascade }, null: false
t.string :css, null: false, default: ''
t.timestamps
end
end
end

View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
ActiveRecord::Schema[7.1].define(version: 2024_08_28_123604) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -479,6 +479,14 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
t.index ["uri"], name: "index_conversations_on_uri", unique: true, opclass: :text_pattern_ops, where: "(uri IS NOT NULL)"
end
create_table "custom_csses", force: :cascade do |t|
t.bigint "user_id", null: false
t.string "css", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_custom_csses_on_user_id"
end
create_table "custom_emoji_categories", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: nil, null: false
@ -1678,6 +1686,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_08_08_125420) do
add_foreign_key "circles", "accounts", on_delete: :cascade
add_foreign_key "conversation_mutes", "accounts", name: "fk_225b4212bb", on_delete: :cascade
add_foreign_key "conversation_mutes", "conversations", on_delete: :cascade
add_foreign_key "custom_csses", "users", on_delete: :cascade
add_foreign_key "custom_filter_keywords", "custom_filters", on_delete: :cascade
add_foreign_key "custom_filter_statuses", "custom_filters", on_delete: :cascade
add_foreign_key "custom_filter_statuses", "statuses", on_delete: :cascade

View file

@ -14,6 +14,7 @@ namespace :dangerous do
end
target_migrations = %w(
20240828123604
20240709063700
20240426233435
20240426000034
@ -113,6 +114,7 @@ namespace :dangerous do
circles
circle_accounts
circle_statuses
custom_csses
emoji_reactions
friend_domains
instance_infos

View file

@ -203,6 +203,45 @@ RSpec.describe Auth::SessionsController do
end
end
end
context 'with custom css' do
let(:params) { {} }
before do
user.settings['web.use_custom_css'] = true
user.save!
post :create, params: { user: { email: user.email, password: user.password }.merge(params) }
end
context 'when does not reset custom css' do
let(:params) { { disable_css: '0' } }
it 'custom css is enabled' do
expect(response).to redirect_to(root_path)
expect(controller.current_user).to eq user
expect(user.reload.setting_use_custom_css).to be true
end
end
context 'when reset custom css' do
let(:params) { { disable_css: '1' } }
it 'custom css is disabled' do
expect(response).to redirect_to(root_path)
expect(controller.current_user).to eq user
expect(user.reload.setting_use_custom_css).to be false
end
end
context 'when does not specify about custom css' do
it 'custom css is enabled' do
expect(response).to redirect_to(root_path)
expect(controller.current_user).to eq user
expect(user.reload.setting_use_custom_css).to be true
end
end
end
end
context 'when using two-factor authentication' do

View file

@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'rails_helper'
describe 'User custom CSS' do
let(:user) { Fabricate(:user) }
let(:custom_css) { '* { display: none !important; }' }
describe 'GET /user_custom.css' do
context 'without sign in' do
it 'returns 422' do
get '/user_custom.css'
expect(response).to have_http_status(401)
end
end
context 'with sign in but custom css is not enabled' do
before do
user.update!(custom_css_text: custom_css)
sign_in user
end
it 'returns custom css' do
get '/user_custom.css'
expect(response).to have_http_status(200)
expect(response.content_type).to include 'text/css'
expect(response.body.strip).to eq custom_css
end
end
context 'with sign in and custom css is enabled' do
before do
user.update!(custom_css_text: custom_css, settings: { 'web.use_custom_css': true })
sign_in user
end
it 'returns custom css' do
get '/user_custom.css'
expect(response).to have_http_status(200)
expect(response.content_type).to include 'text/css'
expect(response.body.strip).to eq custom_css
end
end
end
end