diff options
Diffstat (limited to 'app/controllers')
49 files changed, 676 insertions, 25 deletions
diff --git a/app/controllers/about_controller.rb b/app/controllers/about_controller.rb index d7e78d6b9..620c0ff78 100644 --- a/app/controllers/about_controller.rb +++ b/app/controllers/about_controller.rb @@ -3,6 +3,8 @@ class AboutController < ApplicationController include RegistrationSpamConcern + before_action :set_pack + layout 'public' before_action :require_open_federation!, only: [:show, :more] @@ -54,6 +56,10 @@ class AboutController < ApplicationController end end + def set_pack + use_pack 'public' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index fe7d934dc..9949206cb 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -17,6 +17,7 @@ class AccountsController < ApplicationController def show respond_to do |format| format.html do + use_pack 'public' expires_in 0, public: true unless user_signed_in? @pinned_statuses = [] @@ -64,7 +65,7 @@ class AccountsController < ApplicationController end def filtered_pinned_statuses - @account.pinned_statuses.where(visibility: [:public, :unlisted]) + @account.pinned_statuses.not_local_only.where(visibility: [:public, :unlisted]) end def filtered_statuses @@ -76,7 +77,7 @@ class AccountsController < ApplicationController end def default_statuses - @account.statuses.where(visibility: [:public, :unlisted]) + @account.statuses.not_local_only.where(visibility: [:public, :unlisted]) end def only_media_scope diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index e4e994a98..ac7ab8a0b 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -20,7 +20,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_items case params[:id] when 'featured' - @items = for_signed_account { cache_collection(@account.pinned_statuses, Status) } + @items = for_signed_account { cache_collection(@account.pinned_statuses.not_local_only, Status) } @items = @items.map { |item| item.distributable? ? item : ActivityPub::TagManager.instance.uri_for(item) } when 'tags' @items = for_signed_account { @account.featured_tags } diff --git a/app/controllers/admin/base_controller.rb b/app/controllers/admin/base_controller.rb index 5b7a7ec11..c645ce12b 100644 --- a/app/controllers/admin/base_controller.rb +++ b/app/controllers/admin/base_controller.rb @@ -7,6 +7,7 @@ module Admin layout 'admin' + before_action :set_pack before_action :set_body_classes after_action :verify_authorized @@ -16,6 +17,10 @@ module Admin @body_classes = 'admin' end + def set_pack + use_pack 'admin' + end + def set_user @user = Account.find(params[:account_id]).user || raise(ActiveRecord::RecordNotFound) end diff --git a/app/controllers/admin/custom_emojis_controller.rb b/app/controllers/admin/custom_emojis_controller.rb index 2c33e9f8f..1fae60f5b 100644 --- a/app/controllers/admin/custom_emojis_controller.rb +++ b/app/controllers/admin/custom_emojis_controller.rb @@ -37,6 +37,9 @@ module Admin flash[:alert] = I18n.t('admin.accounts.no_account_selected') rescue Mastodon::NotPermittedError flash[:alert] = I18n.t('admin.custom_emojis.not_permitted') + rescue ActiveRecord::RecordInvalid => e + error_message = action_from_button == 'copy' ? 'admin.custom_emojis.batch_copy_error' : 'admin.custom_emojis.batch_error' + flash[:alert] = I18n.t(error_message, message: e.message) ensure redirect_to admin_custom_emojis_path(filter_params) end diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 16defc1ea..32f1f9a5d 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -4,6 +4,18 @@ module Admin class DomainBlocksController < BaseController before_action :set_domain_block, only: [:show, :destroy, :edit, :update] + def batch + authorize :domain_block, :create? + @form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button)) + @form.save + rescue ActionController::ParameterMissing + flash[:alert] = I18n.t('admin.email_domain_blocks.no_domain_block_selected') + rescue Mastodon::NotPermittedError + flash[:alert] = I18n.t('admin.domain_blocks.created_msg') + else + redirect_to admin_instances_path(limited: '1'), notice: I18n.t('admin.domain_blocks.created_msg') + end + def new authorize :domain_block, :create? @domain_block = DomainBlock.new(domain: params[:_domain]) @@ -76,5 +88,15 @@ module Admin def resource_params params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate) end + + def form_domain_block_batch_params + params.require(:form_domain_block_batch).permit(domain_blocks_attributes: [:enabled, :domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate]) + end + + def action_from_button + if params[:save] + 'save' + end + end end end diff --git a/app/controllers/admin/export_domain_allows_controller.rb b/app/controllers/admin/export_domain_allows_controller.rb new file mode 100644 index 000000000..eb2955ac3 --- /dev/null +++ b/app/controllers/admin/export_domain_allows_controller.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainAllowsController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + ROWS_PROCESSING_LIMIT = 20_000 + + def new + authorize :domain_allow, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_allow, :create? + begin + @import = Admin::Import.new(import_params) + parse_import_data!(export_headers) + + @data.take(ROWS_PROCESSING_LIMIT).each do |row| + domain = row['#domain'].strip + next if DomainAllow.allowed?(domain) + + domain_allow = DomainAllow.new(domain: domain) + log_action :create, domain_allow if domain_allow.save + end + flash[:notice] = I18n.t('admin.domain_allows.created_msg') + rescue ActionController::ParameterMissing + flash[:error] = I18n.t('admin.export_domain_allows.no_file') + end + redirect_to admin_instances_path + end + + private + + def export_filename + 'domain_allows.csv' + end + + def export_headers + %w(#domain) + end + + def export_data + CSV.generate(headers: export_headers, write_headers: true) do |content| + DomainAllow.allowed_domains.each do |instance| + content << [instance.domain] + end + end + end + end +end diff --git a/app/controllers/admin/export_domain_blocks_controller.rb b/app/controllers/admin/export_domain_blocks_controller.rb new file mode 100644 index 000000000..db8863551 --- /dev/null +++ b/app/controllers/admin/export_domain_blocks_controller.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'csv' + +module Admin + class ExportDomainBlocksController < BaseController + include AdminExportControllerConcern + + before_action :set_dummy_import!, only: [:new] + + ROWS_PROCESSING_LIMIT = 20_000 + + def new + authorize :domain_block, :create? + end + + def export + authorize :instance, :index? + send_export_file + end + + def import + authorize :domain_block, :create? + + @import = Admin::Import.new(import_params) + parse_import_data!(export_headers) + + @global_private_comment = I18n.t('admin.export_domain_blocks.import.private_comment_template', source: @import.data_file_name, date: I18n.l(Time.now.utc)) + + @form = Form::DomainBlockBatch.new + @domain_blocks = @data.take(ROWS_PROCESSING_LIMIT).filter_map do |row| + domain = row['#domain'].strip + next if DomainBlock.rule_for(domain).present? + + domain_block = DomainBlock.new(domain: domain, + severity: row['#severity'].strip, + reject_media: row['#reject_media'].strip, + reject_reports: row['#reject_reports'].strip, + private_comment: @global_private_comment, + public_comment: row['#public_comment']&.strip, + obfuscate: row['#obfuscate'].strip) + + domain_block if domain_block.valid? + end + + @warning_domains = Instance.where(domain: @domain_blocks.map(&:domain)).where('EXISTS (SELECT 1 FROM follows JOIN accounts ON follows.account_id = accounts.id OR follows.target_account_id = accounts.id WHERE accounts.domain = instances.domain)').pluck(:domain) + rescue ActionController::ParameterMissing + flash.now[:alert] = I18n.t('admin.export_domain_blocks.no_file') + set_dummy_import! + render :new + end + + private + + def export_filename + 'domain_blocks.csv' + end + + def export_headers + %w(#domain #severity #reject_media #reject_reports #public_comment #obfuscate) + end + + def export_data + CSV.generate(headers: export_headers, write_headers: true) do |content| + DomainBlock.with_user_facing_limitations.each do |instance| + content << [instance.domain, instance.severity, instance.reject_media, instance.reject_reports, instance.public_comment, instance.obfuscate] + end + end + end + end +end diff --git a/app/controllers/api/v1/accounts/relationships_controller.rb b/app/controllers/api/v1/accounts/relationships_controller.rb index 503f85c97..1d3992a28 100644 --- a/app/controllers/api/v1/accounts/relationships_controller.rb +++ b/app/controllers/api/v1/accounts/relationships_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Accounts::RelationshipsController < Api::BaseController before_action :require_user! def index - accounts = Account.without_suspended.where(id: account_ids).select('id') + accounts = Account.where(id: account_ids).select('id') # .where doesn't guarantee that our results are in the same order # we requested them, so return the "right" order to the requestor. @accounts = accounts.index_by(&:id).values_at(*account_ids).compact diff --git a/app/controllers/api/v1/notifications_controller.rb b/app/controllers/api/v1/notifications_controller.rb index 6d464997e..ac49167cb 100644 --- a/app/controllers/api/v1/notifications_controller.rb +++ b/app/controllers/api/v1/notifications_controller.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true class Api::V1::NotificationsController < Api::BaseController - before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss] - before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss] + before_action -> { doorkeeper_authorize! :read, :'read:notifications' }, except: [:clear, :dismiss, :destroy, :destroy_multiple] + before_action -> { doorkeeper_authorize! :write, :'write:notifications' }, only: [:clear, :dismiss, :destroy, :destroy_multiple] before_action :require_user! after_action :insert_pagination_headers, only: :index @@ -23,11 +23,20 @@ class Api::V1::NotificationsController < Api::BaseController render_empty end + def destroy + dismiss + end + def dismiss current_account.notifications.find_by!(id: params[:id]).destroy! render_empty end + def destroy_multiple + current_account.notifications.where(id: params[:ids]).destroy_all + render_empty + end + private def load_notifications diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 9270117da..b2cee3e92 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -48,6 +48,7 @@ class Api::V1::StatusesController < Api::BaseController scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, poll: status_params[:poll], + content_type: status_params[:content_type], idempotency: request.headers['Idempotency-Key'], with_rate_limit: true ) @@ -66,7 +67,8 @@ class Api::V1::StatusesController < Api::BaseController media_ids: status_params[:media_ids], sensitive: status_params[:sensitive], spoiler_text: status_params[:spoiler_text], - poll: status_params[:poll] + poll: status_params[:poll], + content_type: status_params[:content_type] ) render json: @status, serializer: REST::StatusSerializer @@ -110,6 +112,7 @@ class Api::V1::StatusesController < Api::BaseController :visibility, :language, :scheduled_at, + :content_type, media_ids: [], poll: [ :multiple, diff --git a/app/controllers/api/v1/timelines/direct_controller.rb b/app/controllers/api/v1/timelines/direct_controller.rb new file mode 100644 index 000000000..6e98e9cac --- /dev/null +++ b/app/controllers/api/v1/timelines/direct_controller.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class Api::V1::Timelines::DirectController < Api::BaseController + before_action -> { doorkeeper_authorize! :read, :'read:statuses' }, only: [:show] + before_action :require_user!, only: [:show] + after_action :insert_pagination_headers, unless: -> { @statuses.empty? } + + respond_to :json + + def show + @statuses = load_statuses + render json: @statuses, each_serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id) + end + + private + + def load_statuses + cached_direct_statuses + end + + def cached_direct_statuses + cache_collection direct_statuses, Status + end + + def direct_statuses + direct_timeline_statuses + end + + def direct_timeline_statuses + account_direct_feed.get( + limit_param(DEFAULT_STATUSES_LIMIT), + params[:max_id], + params[:since_id], + params[:min_id] + ) + end + + def account_direct_feed + DirectFeed.new(current_account) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def pagination_params(core_params) + params.permit(:local, :limit).merge(core_params) + end + + def next_path + api_v1_timelines_direct_url pagination_params(max_id: pagination_max_id) + end + + def prev_path + api_v1_timelines_direct_url pagination_params(since_id: pagination_since_id) + end + + def pagination_max_id + @statuses.last.id + end + + def pagination_since_id + @statuses.first.id + end +end diff --git a/app/controllers/api/v1/timelines/public_controller.rb b/app/controllers/api/v1/timelines/public_controller.rb index d253b744f..493fe4776 100644 --- a/app/controllers/api/v1/timelines/public_controller.rb +++ b/app/controllers/api/v1/timelines/public_controller.rb @@ -37,7 +37,10 @@ class Api::V1::Timelines::PublicController < Api::BaseController current_account, local: truthy_param?(:local), remote: truthy_param?(:remote), - only_media: truthy_param?(:only_media) + only_media: truthy_param?(:only_media), + allow_local_only: truthy_param?(:allow_local_only), + with_replies: Setting.show_replies_in_public_timelines, + with_reblogs: Setting.show_reblogs_in_public_timelines, ) end @@ -46,7 +49,7 @@ class Api::V1::Timelines::PublicController < Api::BaseController end def pagination_params(core_params) - params.slice(:local, :remote, :limit, :only_media).permit(:local, :remote, :limit, :only_media).merge(core_params) + params.slice(:local, :remote, :limit, :only_media, :allow_local_only).permit(:local, :remote, :limit, :only_media, :allow_local_only).merge(core_params) end def next_path diff --git a/app/controllers/api/v2/search_controller.rb b/app/controllers/api/v2/search_controller.rb index a30560133..77eeab5b0 100644 --- a/app/controllers/api/v2/search_controller.rb +++ b/app/controllers/api/v2/search_controller.rb @@ -3,7 +3,7 @@ class Api::V2::SearchController < Api::BaseController include Authorization - RESULTS_LIMIT = 20 + RESULTS_LIMIT = (ENV['MAX_SEARCH_RESULTS'] || 20).to_i before_action -> { doorkeeper_authorize! :read, :'read:search' } before_action :require_user! diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 615536b96..ee3c5204d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,10 +10,12 @@ class ApplicationController < ActionController::Base include SessionTrackingConcern include CacheConcern include DomainControlHelper + include ThemingConcern helper_method :current_account helper_method :current_session - helper_method :current_theme + helper_method :current_flavour + helper_method :current_skin helper_method :single_user_mode? helper_method :use_seamless_external_login? helper_method :whitelist_mode? @@ -126,14 +128,12 @@ class ApplicationController < ActionController::Base @current_session = SessionActivation.find_by(session_id: cookies.signed['_session_id']) if cookies.signed['_session_id'].present? end - def current_theme - return Setting.theme unless Themes.instance.names.include? current_user&.setting_theme - current_user.setting_theme - end - def respond_with_error(code) respond_to do |format| - format.any { render "errors/#{code}", layout: 'error', status: code, formats: [:html] } + format.any do + use_pack 'error' + render "errors/#{code}", layout: 'error', status: code, formats: [:html] + end format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code } end end diff --git a/app/controllers/auth/challenges_controller.rb b/app/controllers/auth/challenges_controller.rb index 060944240..41827b21c 100644 --- a/app/controllers/auth/challenges_controller.rb +++ b/app/controllers/auth/challenges_controller.rb @@ -5,6 +5,7 @@ class Auth::ChallengesController < ApplicationController layout 'auth' + before_action :set_pack before_action :authenticate_user! skip_before_action :require_functional! @@ -19,4 +20,10 @@ class Auth::ChallengesController < ApplicationController render_challenge end end + + private + + def set_pack + use_pack 'auth' + end end diff --git a/app/controllers/auth/confirmations_controller.rb b/app/controllers/auth/confirmations_controller.rb index 010fd3755..0817a905c 100644 --- a/app/controllers/auth/confirmations_controller.rb +++ b/app/controllers/auth/confirmations_controller.rb @@ -1,11 +1,18 @@ # frozen_string_literal: true class Auth::ConfirmationsController < Devise::ConfirmationsController + include CaptchaConcern + layout 'auth' before_action :set_body_classes + before_action :set_pack + before_action :set_confirmation_user!, only: [:show, :confirm_captcha] before_action :require_unconfirmed! + before_action :extend_csp_for_captcha!, only: [:show, :confirm_captcha] + before_action :require_captcha_if_needed!, only: [:show] + skip_before_action :require_functional! def new @@ -14,8 +21,50 @@ class Auth::ConfirmationsController < Devise::ConfirmationsController resource.email = current_user.unconfirmed_email || current_user.email if user_signed_in? end + def show + old_session_values = session.to_hash + reset_session + session.update old_session_values.except('session_id') + + super + end + + def confirm_captcha + check_captcha! do |message| + flash.now[:alert] = message + render :captcha + return + end + + show + end + private + def require_captcha_if_needed! + render :captcha if captcha_required? + end + + def set_confirmation_user! + # We need to reimplement looking up the user because + # Devise::ConfirmationsController#show looks up and confirms in one + # step. + confirmation_token = params[:confirmation_token] + return if confirmation_token.nil? + @confirmation_user = User.find_first_by_auth_conditions(confirmation_token: confirmation_token) + end + + def captcha_user_bypass? + return true if @confirmation_user.nil? || @confirmation_user.confirmed? + + invite = Invite.find(@confirmation_user.invite_id) if @confirmation_user.invite_id.present? + invite.present? && !invite.max_uses.nil? + end + + def set_pack + use_pack 'auth' + end + def require_unconfirmed! if user_signed_in? && current_user.confirmed? && current_user.unconfirmed_email.blank? redirect_to(current_user.approved? ? root_path : edit_user_registration_path) diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 2996c0431..609220eb1 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -2,6 +2,7 @@ class Auth::PasswordsController < Devise::PasswordsController before_action :check_validity_of_reset_password_token, only: :edit + before_action :set_pack before_action :set_body_classes layout 'auth' @@ -30,4 +31,8 @@ class Auth::PasswordsController < Devise::PasswordsController def reset_password_token_is_valid? resource_class.with_reset_password_token(params[:reset_password_token]).present? end + + def set_pack + use_pack 'auth' + end end diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb index 7e86e01ba..486edcdcb 100644 --- a/app/controllers/auth/registrations_controller.rb +++ b/app/controllers/auth/registrations_controller.rb @@ -8,6 +8,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController before_action :set_invite, only: [:new, :create] before_action :check_enabled_registrations, only: [:new, :create] before_action :configure_sign_up_params, only: [:create] + before_action :set_pack before_action :set_sessions, only: [:edit, :update] before_action :set_strikes, only: [:edit, :update] before_action :set_instance_presenter, only: [:new, :create, :update] @@ -107,6 +108,10 @@ class Auth::RegistrationsController < Devise::RegistrationsController private + def set_pack + use_pack %w(edit update).include?(action_name) ? 'admin' : 'auth' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index f9a55eb4b..13dfebcdd 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -7,6 +7,7 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_functional! skip_before_action :update_user_sign_in + prepend_before_action :set_pack prepend_before_action :check_suspicious!, only: [:create] include TwoFactorAuthenticationConcern @@ -95,6 +96,10 @@ class Auth::SessionsController < Devise::SessionsController private + def set_pack + use_pack 'auth' + end + def set_instance_presenter @instance_presenter = InstancePresenter.new end diff --git a/app/controllers/auth/setup_controller.rb b/app/controllers/auth/setup_controller.rb index 46c5f2958..db5a866f2 100644 --- a/app/controllers/auth/setup_controller.rb +++ b/app/controllers/auth/setup_controller.rb @@ -3,6 +3,7 @@ class Auth::SetupController < ApplicationController layout 'auth' + before_action :set_pack before_action :authenticate_user! before_action :require_unconfirmed_or_pending! before_action :set_body_classes @@ -55,4 +56,8 @@ class Auth::SetupController < ApplicationController def missing_email? truthy_param?(:missing_email) end + + def set_pack + use_pack 'auth' + end end diff --git a/app/controllers/authorize_interactions_controller.rb b/app/controllers/authorize_interactions_controller.rb index 02a6b6d06..97fe4a9ab 100644 --- a/app/controllers/authorize_interactions_controller.rb +++ b/app/controllers/authorize_interactions_controller.rb @@ -8,6 +8,7 @@ class AuthorizeInteractionsController < ApplicationController before_action :authenticate_user! before_action :set_body_classes before_action :set_resource + before_action :set_pack def show if @resource.is_a?(Account) @@ -65,4 +66,8 @@ class AuthorizeInteractionsController < ApplicationController def set_body_classes @body_classes = 'modal-layout' end + + def set_pack + use_pack 'modal' + end end diff --git a/app/controllers/concerns/admin_export_controller_concern.rb b/app/controllers/concerns/admin_export_controller_concern.rb new file mode 100644 index 000000000..013915d02 --- /dev/null +++ b/app/controllers/concerns/admin_export_controller_concern.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module AdminExportControllerConcern + extend ActiveSupport::Concern + + private + + def send_export_file + respond_to do |format| + format.csv { send_data export_data, filename: export_filename } + end + end + + def export_data + raise 'Override in controller' + end + + def export_filename + raise 'Override in controller' + end + + def set_dummy_import! + @import = Admin::Import.new + end + + def import_params + params.require(:admin_import).permit(:data) + end + + def import_data + Paperclip.io_adapters.for(@import.data).read + end + + def parse_import_data!(default_headers) + data = CSV.parse(import_data, headers: true) + data = CSV.parse(import_data, headers: default_headers) unless data.headers&.first&.strip&.include?(default_headers[0]) + @data = data.reject(&:blank?) + end +end diff --git a/app/controllers/concerns/captcha_concern.rb b/app/controllers/concerns/captcha_concern.rb new file mode 100644 index 000000000..538c1ffb1 --- /dev/null +++ b/app/controllers/concerns/captcha_concern.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module CaptchaConcern + extend ActiveSupport::Concern + include Hcaptcha::Adapters::ViewMethods + + included do + helper_method :render_captcha + end + + def captcha_available? + ENV['HCAPTCHA_SECRET_KEY'].present? && ENV['HCAPTCHA_SITE_KEY'].present? + end + + def captcha_enabled? + captcha_available? && Setting.captcha_enabled + end + + def captcha_user_bypass? + false + end + + def captcha_required? + captcha_enabled? && !captcha_user_bypass? + end + + def check_captcha! + return true unless captcha_required? + + if verify_hcaptcha + true + else + if block_given? + message = flash[:hcaptcha_error] + flash.delete(:hcaptcha_error) + yield message + end + false + end + end + + def extend_csp_for_captcha! + policy = request.content_security_policy + return unless captcha_required? && policy.present? + + %w(script_src frame_src style_src connect_src).each do |directive| + values = policy.send(directive) + values << 'https://hcaptcha.com' unless values.include?('https://hcaptcha.com') || values.include?('https:') + values << 'https://*.hcaptcha.com' unless values.include?('https://*.hcaptcha.com') || values.include?('https:') + policy.send(directive, *values) + end + end + + def render_captcha + return unless captcha_required? + + hcaptcha_tags + end +end diff --git a/app/controllers/concerns/theming_concern.rb b/app/controllers/concerns/theming_concern.rb new file mode 100644 index 000000000..f993a81d7 --- /dev/null +++ b/app/controllers/concerns/theming_concern.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +module ThemingConcern + extend ActiveSupport::Concern + + def use_pack(pack_name) + @core = resolve_pack_with_common(Themes.instance.core, pack_name) + @theme = resolve_pack_with_common(Themes.instance.flavour(current_flavour), pack_name, current_skin) + end + + private + + def current_flavour + [current_user&.setting_flavour, Setting.flavour, 'glitch', 'vanilla'].find { |flavour| Themes.instance.flavours.include?(flavour) } + end + + def current_skin + skins = Themes.instance.skins_for(current_flavour) + [current_user&.setting_skin, Setting.skin, 'default'].find { |skin| skins.include?(skin) } + end + + def valid_pack_data?(data, pack_name) + data['pack'].is_a?(Hash) && data['pack'][pack_name].present? + end + + def nil_pack(data) + { + use_common: true, + flavour: data['name'], + pack: nil, + preload: nil, + skin: nil, + supported_locales: data['locales'], + } + end + + def pack(data, pack_name, skin) + pack_data = { + use_common: true, + flavour: data['name'], + pack: pack_name, + preload: nil, + skin: nil, + supported_locales: data['locales'], + } + + return pack_data unless data['pack'][pack_name].is_a?(Hash) + + pack_data[:use_common] = false if data['pack'][pack_name]['use_common'] == false + pack_data[:pack] = nil unless data['pack'][pack_name]['filename'] + + preloads = data['pack'][pack_name]['preload'] + pack_data[:preload] = [preloads] if preloads.is_a?(String) + pack_data[:preload] = preloads if preloads.is_a?(Array) + + if skin != 'default' && data['skin'][skin] + pack_data[:skin] = skin if data['skin'][skin].include?(pack_name) + elsif data['pack'][pack_name]['stylesheet'] + pack_data[:skin] = 'default' + end + + pack_data + end + + def resolve_pack(data, pack_name, skin) + return pack(data, pack_name, skin) if valid_pack_data?(data, pack_name) + return if data['name'].blank? + + fallbacks = [] + if data.key?('fallback') + fallbacks = data['fallback'] if data['fallback'].is_a?(Array) + fallbacks = [data['fallback']] if data['fallback'].is_a?(String) + elsif data['name'] != Setting.default_settings['flavour'] + fallbacks = [Setting.default_settings['flavour']] + end + + fallbacks.each do |fallback| + return resolve_pack(Themes.instance.flavour(fallback), pack_name) if Themes.instance.flavour(fallback) + end + + nil + end + + def resolve_pack_with_common(data, pack_name, skin = 'default') + result = resolve_pack(data, pack_name, skin) || nil_pack(data) + result[:common] = resolve_pack(data, 'common', skin) if result.delete(:use_common) + result + end +end diff --git a/app/controllers/concerns/two_factor_authentication_concern.rb b/app/controllers/concerns/two_factor_authentication_concern.rb index 27f2367a8..c9477a1d4 100644 --- a/app/controllers/concerns/two_factor_authentication_concern.rb +++ b/app/controllers/concerns/two_factor_authentication_concern.rb @@ -77,6 +77,8 @@ module TwoFactorAuthenticationConcern def prompt_for_two_factor(user) set_attempt_session(user) + use_pack 'auth' + @body_classes = 'lighter' @webauthn_enabled = user.webauthn_enabled? @scheme_type = begin diff --git a/app/controllers/directories_controller.rb b/app/controllers/directories_controller.rb index f28c5b2af..2263f286b 100644 --- a/app/controllers/directories_controller.rb +++ b/app/controllers/directories_controller.rb @@ -7,6 +7,7 @@ class DirectoriesController < ApplicationController before_action :require_enabled! before_action :set_instance_presenter before_action :set_accounts + before_action :set_pack skip_before_action :require_functional!, unless: :whitelist_mode? @@ -16,6 +17,10 @@ class DirectoriesController < ApplicationController private + def set_pack + use_pack 'share' + end + def require_enabled! return not_found unless Setting.profile_directory end diff --git a/app/controllers/disputes/base_controller.rb b/app/controllers/disputes/base_controller.rb index 865146b5c..7830c5524 100644 --- a/app/controllers/disputes/base_controller.rb +++ b/app/controllers/disputes/base_controller.rb @@ -9,9 +9,14 @@ class Disputes::BaseController < ApplicationController before_action :set_body_classes before_action :authenticate_user! + before_action :set_pack private + def set_pack + use_pack 'admin' + end + def set_body_classes @body_classes = 'admin' end diff --git a/app/controllers/filters_controller.rb b/app/controllers/filters_controller.rb index cc5cb5d9f..2ab3b0a74 100644 --- a/app/controllers/filters_controller.rb +++ b/app/controllers/filters_controller.rb @@ -5,6 +5,7 @@ class FiltersController < ApplicationController before_action :authenticate_user! before_action :set_filter, only: [:edit, :update, :destroy] + before_action :set_pack before_action :set_body_classes def index @@ -43,6 +44,10 @@ class FiltersController < ApplicationController private + def set_pack + use_pack 'settings' + end + def set_filter @filter = current_account.custom_filters.find(params[:id]) end diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f3f8336c9..f898994ac 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -13,6 +13,7 @@ class FollowerAccountsController < ApplicationController def index respond_to do |format| format.html do + use_pack 'public' expires_in 0, public: true unless user_signed_in? next if @account.hide_collections? @@ -61,22 +62,22 @@ class FollowerAccountsController < ApplicationController end def collection_presenter + options = { type: :ordered } + options[:size] = @account.followers_count unless Setting.hide_followers_count || @account.user&.setting_hide_followers_count if page_requested? ActivityPub::CollectionPresenter.new( id: account_followers_url(@account, page: params.fetch(:page, 1)), - type: :ordered, - size: @account.followers_count, items: follows.map { |f| ActivityPub::TagManager.instance.uri_for(f.account) }, part_of: account_followers_url(@account), next: next_page_url, - prev: prev_page_url + prev: prev_page_url, + **options ) else ActivityPub::CollectionPresenter.new( id: account_followers_url(@account), - type: :ordered, - size: @account.followers_count, - first: page_url(1) + first: page_url(1), + **options ) end end diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 69f0321f8..11c6b6d50 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -13,6 +13,7 @@ class FollowingAccountsController < ApplicationController def index respond_to do |format| format.html do + use_pack 'public' expires_in 0, public: true unless user_signed_in? next if @account.hide_collections? diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 7e443eb9e..450f92bd4 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -3,6 +3,8 @@ class HomeController < ApplicationController before_action :redirect_unauthenticated_to_permalinks! before_action :authenticate_user! + + before_action :set_pack before_action :set_referrer_policy_header def index @@ -17,6 +19,10 @@ class HomeController < ApplicationController redirect_to(PermalinkRedirector.new(request.path).redirect_path || default_redirect_path) end + def set_pack + use_pack 'home' + end + def default_redirect_path if request.path.start_with?('/web') || whitelist_mode? new_user_session_path diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 8d92147e2..0b3c082dc 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -6,6 +6,7 @@ class InvitesController < ApplicationController layout 'admin' before_action :authenticate_user! + before_action :set_pack before_action :set_body_classes def index @@ -38,6 +39,10 @@ class InvitesController < ApplicationController private + def set_pack + use_pack 'settings' + end + def invites current_user.invites.order(id: :desc) end diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index ee82625a0..d2de432ba 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -11,6 +11,7 @@ class MediaController < ApplicationController before_action :verify_permitted_status! before_action :check_playable, only: :player before_action :allow_iframing, only: :player + before_action :set_pack, only: :player content_security_policy only: :player do |p| p.frame_ancestors(false) @@ -48,4 +49,8 @@ class MediaController < ApplicationController def allow_iframing response.headers['X-Frame-Options'] = 'ALLOWALL' end + + def set_pack + use_pack 'public' + end end diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index bb5d639ce..137346ed0 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController before_action :store_current_location before_action :authenticate_resource_owner! + before_action :set_pack before_action :set_cache_headers include Localized @@ -15,6 +16,10 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController store_location_for(:user, request.url) end + def set_pack + use_pack 'auth' + end + def render_success if skip_authorization? || (matching_token? && !truthy_param?('force_login')) redirect_or_render authorize_response diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 45151cdd7..b2564a791 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -5,6 +5,7 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio before_action :store_current_location before_action :authenticate_resource_owner! + before_action :set_pack before_action :require_not_suspended!, only: :destroy before_action :set_body_classes @@ -27,6 +28,10 @@ class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicatio store_location_for(:user, request.url) end + def set_pack + use_pack 'settings' + end + def require_not_suspended! forbidden if current_account.suspended? end diff --git a/app/controllers/public_timelines_controller.rb b/app/controllers/public_timelines_controller.rb index 1332ba16c..eb5bb191b 100644 --- a/app/controllers/public_timelines_controller.rb +++ b/app/controllers/public_timelines_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class PublicTimelinesController < ApplicationController + before_action :set_pack layout 'public' before_action :authenticate_user!, if: :whitelist_mode? @@ -23,4 +24,8 @@ class PublicTimelinesController < ApplicationController def set_instance_presenter @instance_presenter = InstancePresenter.new end + + def set_pack + use_pack 'about' + end end diff --git a/app/controllers/relationships_controller.rb b/app/controllers/relationships_controller.rb index 96cce55e9..d40770726 100644 --- a/app/controllers/relationships_controller.rb +++ b/app/controllers/relationships_controller.rb @@ -5,6 +5,7 @@ class RelationshipsController < ApplicationController before_action :authenticate_user! before_action :set_accounts, only: :show + before_action :set_pack before_action :set_relationships, only: :show before_action :set_body_classes @@ -68,4 +69,8 @@ class RelationshipsController < ApplicationController def set_body_classes @body_classes = 'admin' end + + def set_pack + use_pack 'admin' + end end diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index db1604644..93a0a7476 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -5,6 +5,7 @@ class RemoteFollowController < ApplicationController layout 'modal' + before_action :set_pack before_action :set_body_classes skip_before_action :require_functional! @@ -34,6 +35,10 @@ class RemoteFollowController < ApplicationController { acct: session[:remote_follow] || current_account&.username } end + def set_pack + use_pack 'modal' + end + def set_body_classes @body_classes = 'modal-layout' @hide_header = true diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index 6c29a2b9f..a277bfa10 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -9,6 +9,7 @@ class RemoteInteractionController < ApplicationController before_action :set_interaction_type before_action :set_status before_action :set_body_classes + before_action :set_pack skip_before_action :require_functional!, unless: :whitelist_mode? @@ -49,6 +50,10 @@ class RemoteInteractionController < ApplicationController @hide_header = true end + def set_pack + use_pack 'modal' + end + def set_interaction_type @interaction_type = %w(reply reblog favourite).include?(params[:type]) ? params[:type] : 'reply' end diff --git a/app/controllers/settings/base_controller.rb b/app/controllers/settings/base_controller.rb index 8311538a5..dee3922d8 100644 --- a/app/controllers/settings/base_controller.rb +++ b/app/controllers/settings/base_controller.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class Settings::BaseController < ApplicationController + before_action :set_pack layout 'admin' before_action :authenticate_user! @@ -9,6 +10,10 @@ class Settings::BaseController < ApplicationController private + def set_pack + use_pack 'settings' + end + def set_body_classes @body_classes = 'admin' end diff --git a/app/controllers/settings/flavours_controller.rb b/app/controllers/settings/flavours_controller.rb new file mode 100644 index 000000000..62c52eee9 --- /dev/null +++ b/app/controllers/settings/flavours_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class Settings::FlavoursController < Settings::BaseController + layout 'admin' + + before_action :authenticate_user! + + skip_before_action :require_functional! + + def index + redirect_to action: 'show', flavour: current_flavour + end + + def show + unless Themes.instance.flavours.include?(params[:flavour]) || (params[:flavour] == current_flavour) + redirect_to action: 'show', flavour: current_flavour + end + + @listing = Themes.instance.flavours + @selected = params[:flavour] + end + + def update + user_settings.update(user_settings_params) + redirect_to action: 'show', flavour: params[:flavour] + end + + private + + def user_settings + UserSettingsDecorator.new(current_user) + end + + def user_settings_params + { setting_flavour: params.require(:flavour), + setting_skin: params.dig(:user, :setting_skin) }.with_indifferent_access + end +end diff --git a/app/controllers/settings/login_activities_controller.rb b/app/controllers/settings/login_activities_controller.rb index 57fa6aef0..ee77524b1 100644 --- a/app/controllers/settings/login_activities_controller.rb +++ b/app/controllers/settings/login_activities_controller.rb @@ -4,4 +4,10 @@ class Settings::LoginActivitiesController < Settings::BaseController def index @login_activities = LoginActivity.where(user: current_user).order(id: :desc).page(params[:page]) end + + private + + def set_pack + use_pack 'settings' + end end diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index bfe651bc6..669ed00c6 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -38,6 +38,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_default_language, :setting_unfollow_modal, :setting_boost_modal, + :setting_favourite_modal, :setting_delete_modal, :setting_auto_play_gif, :setting_display_media, @@ -45,17 +46,19 @@ class Settings::PreferencesController < Settings::BaseController :setting_reduce_motion, :setting_disable_swiping, :setting_system_font_ui, + :setting_system_emoji_font, :setting_noindex, - :setting_theme, + :setting_hide_followers_count, :setting_aggregate_reblogs, :setting_show_application, :setting_advanced_layout, + :setting_default_content_type, :setting_use_blurhash, :setting_use_pending_items, :setting_trends, :setting_crop_images, :setting_always_send_emails, - notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag appeal), + notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account trending_tag trending_link trending_status appeal), interactions: %i(must_be_follower must_be_following must_be_following_dm) ) end diff --git a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb index a50d30f06..7e2d43dcd 100644 --- a/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb +++ b/app/controllers/settings/two_factor_authentication/webauthn_credentials_controller.rb @@ -85,6 +85,10 @@ module Settings private + def set_pack + use_pack 'auth' + end + def require_otp_enabled unless current_user.otp_enabled? flash[:error] = t('webauthn_credentials.otp_required') diff --git a/app/controllers/shares_controller.rb b/app/controllers/shares_controller.rb index 6546b8497..e13e7e8b6 100644 --- a/app/controllers/shares_controller.rb +++ b/app/controllers/shares_controller.rb @@ -4,12 +4,17 @@ class SharesController < ApplicationController layout 'modal' before_action :authenticate_user! + before_action :set_pack before_action :set_body_classes def show; end private + def set_pack + use_pack 'share' + end + def set_body_classes @body_classes = 'modal-layout compose-standalone' end diff --git a/app/controllers/statuses_cleanup_controller.rb b/app/controllers/statuses_cleanup_controller.rb index be234cdcb..3d4f4af02 100644 --- a/app/controllers/statuses_cleanup_controller.rb +++ b/app/controllers/statuses_cleanup_controller.rb @@ -6,6 +6,7 @@ class StatusesCleanupController < ApplicationController before_action :authenticate_user! before_action :set_policy before_action :set_body_classes + before_action :set_pack def show; end @@ -21,6 +22,10 @@ class StatusesCleanupController < ApplicationController private + def set_pack + use_pack 'settings' + end + def set_policy @policy = current_account.statuses_cleanup_policy || current_account.build_statuses_cleanup_policy(enabled: false) end diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index c52170d08..3812f541e 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -27,6 +27,8 @@ class StatusesController < ApplicationController def show respond_to do |format| format.html do + use_pack 'public' + expires_in 10.seconds, public: true if current_account.nil? set_ancestors set_descendants @@ -45,6 +47,7 @@ class StatusesController < ApplicationController end def embed + use_pack 'embed' return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index b82da8f0c..46821a200 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -21,6 +21,7 @@ class TagsController < ApplicationController def show respond_to do |format| format.html do + use_pack 'about' expires_in 0, public: true end |