diff --git a/Gemfile.lock b/Gemfile.lock index a42d04c6cb..44f20cdec6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -39,47 +39,47 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (7.0.7.2) - actionpack (= 7.0.7.2) - activesupport (= 7.0.7.2) + actioncable (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.7.2) - actionpack (= 7.0.7.2) - activejob (= 7.0.7.2) - activerecord (= 7.0.7.2) - activestorage (= 7.0.7.2) - activesupport (= 7.0.7.2) + actionmailbox (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.7.2) - actionpack (= 7.0.7.2) - actionview (= 7.0.7.2) - activejob (= 7.0.7.2) - activesupport (= 7.0.7.2) + actionmailer (7.0.8) + actionpack (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activesupport (= 7.0.8) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.7.2) - actionview (= 7.0.7.2) - activesupport (= 7.0.7.2) + actionpack (7.0.8) + actionview (= 7.0.8) + activesupport (= 7.0.8) rack (~> 2.0, >= 2.2.4) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (7.0.7.2) - actionpack (= 7.0.7.2) - activerecord (= 7.0.7.2) - activestorage (= 7.0.7.2) - activesupport (= 7.0.7.2) + actiontext (7.0.8) + actionpack (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.7.2) - activesupport (= 7.0.7.2) + actionview (7.0.8) + activesupport (= 7.0.8) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) @@ -89,22 +89,22 @@ GEM activemodel (>= 4.1, < 7.1) case_transform (>= 0.2) jsonapi-renderer (>= 0.1.1.beta1, < 0.3) - activejob (7.0.7.2) - activesupport (= 7.0.7.2) + activejob (7.0.8) + activesupport (= 7.0.8) globalid (>= 0.3.6) - activemodel (7.0.7.2) - activesupport (= 7.0.7.2) - activerecord (7.0.7.2) - activemodel (= 7.0.7.2) - activesupport (= 7.0.7.2) - activestorage (7.0.7.2) - actionpack (= 7.0.7.2) - activejob (= 7.0.7.2) - activerecord (= 7.0.7.2) - activesupport (= 7.0.7.2) + activemodel (7.0.8) + activesupport (= 7.0.8) + activerecord (7.0.8) + activemodel (= 7.0.8) + activesupport (= 7.0.8) + activestorage (7.0.8) + actionpack (= 7.0.8) + activejob (= 7.0.8) + activerecord (= 7.0.8) + activesupport (= 7.0.8) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.7.2) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -556,20 +556,20 @@ GEM rack rack-test (2.1.0) rack (>= 1.3) - rails (7.0.7.2) - actioncable (= 7.0.7.2) - actionmailbox (= 7.0.7.2) - actionmailer (= 7.0.7.2) - actionpack (= 7.0.7.2) - actiontext (= 7.0.7.2) - actionview (= 7.0.7.2) - activejob (= 7.0.7.2) - activemodel (= 7.0.7.2) - activerecord (= 7.0.7.2) - activestorage (= 7.0.7.2) - activesupport (= 7.0.7.2) + rails (7.0.8) + actioncable (= 7.0.8) + actionmailbox (= 7.0.8) + actionmailer (= 7.0.8) + actionpack (= 7.0.8) + actiontext (= 7.0.8) + actionview (= 7.0.8) + activejob (= 7.0.8) + activemodel (= 7.0.8) + activerecord (= 7.0.8) + activestorage (= 7.0.8) + activesupport (= 7.0.8) bundler (>= 1.15.0) - railties (= 7.0.7.2) + railties (= 7.0.8) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -584,9 +584,9 @@ GEM rails-i18n (7.0.7) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.7.2) - actionpack (= 7.0.7.2) - activesupport (= 7.0.7.2) + railties (7.0.8) + actionpack (= 7.0.8) + activesupport (= 7.0.8) method_source rake (>= 12.2) thor (~> 1.0) @@ -747,7 +747,7 @@ GEM unicode-display_width (>= 1.1.1, < 3) terrapin (0.6.0) climate_control (>= 0.0.3, < 1.0) - test-prof (1.2.2) + test-prof (1.2.3) thor (1.2.2) tilt (2.2.0) timeout (0.4.0) diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb new file mode 100644 index 0000000000..6a7c9f5bf3 --- /dev/null +++ b/app/controllers/api/v1/admin/tags_controller.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class Api::V1::Admin::TagsController < Api::BaseController + include Authorization + before_action -> { authorize_if_got_token! :'admin:read' }, only: [:index, :show] + before_action -> { authorize_if_got_token! :'admin:write' }, only: :update + + before_action :set_tags, only: :index + before_action :set_tag, except: :index + + after_action :insert_pagination_headers, only: :index + after_action :verify_authorized + + LIMIT = 100 + PAGINATION_PARAMS = %i(limit).freeze + + def index + authorize :tag, :index? + render json: @tags, each_serializer: REST::Admin::TagSerializer + end + + def show + authorize @tag, :show? + render json: @tag, serializer: REST::Admin::TagSerializer + end + + def update + authorize @tag, :update? + @tag.update!(tag_params.merge(reviewed_at: Time.now.utc)) + render json: @tag, serializer: REST::Admin::TagSerializer + end + + private + + def set_tag + @tag = Tag.find(params[:id]) + end + + def set_tags + @tags = Tag.all.to_a_paginated_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def tag_params + params.permit(:display_name, :trendable, :usable, :listable) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_tags_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_tags_url(pagination_params(min_id: pagination_since_id)) unless @tags.empty? + end + + def pagination_max_id + @tags.last.id + end + + def pagination_since_id + @tags.first.id + end + + def records_continue? + @tags.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end +end diff --git a/app/controllers/concerns/web_app_controller_concern.rb b/app/controllers/concerns/web_app_controller_concern.rb index 3a40ea3823..273d7344ca 100644 --- a/app/controllers/concerns/web_app_controller_concern.rb +++ b/app/controllers/concerns/web_app_controller_concern.rb @@ -11,7 +11,7 @@ module WebAppControllerConcern end def skip_csrf_meta_tags? - !(ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil? + !(ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1) && current_user.nil? end def set_app_body_class diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 25e4410c65..e80131f979 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -18,6 +18,7 @@ import { importFetchedStatuses, } from './importer'; import { submitMarkers } from './markers'; +import { register as registerPushNotifications } from './push_notifications'; import { saveSettings } from './settings'; import { STATUS_EMOJI_REACTION_UPDATE } from './statuses'; @@ -305,6 +306,10 @@ export function requestBrowserPermission(callback = noOp) { requestNotificationPermission((permission) => { dispatch(setBrowserPermission(permission)); callback(permission); + + if (permission === 'granted') { + dispatch(registerPushNotifications()); + } }); }; } diff --git a/app/javascript/mastodon/main.jsx b/app/javascript/mastodon/main.jsx index f826036318..cd73cb572e 100644 --- a/app/javascript/mastodon/main.jsx +++ b/app/javascript/mastodon/main.jsx @@ -33,7 +33,7 @@ function main() { console.error(err); } - if (registration) { + if (registration && 'Notification' in window && Notification.permission === 'granted') { const registerPushNotifications = await import('mastodon/actions/push_notifications'); store.dispatch(registerPushNotifications.register()); diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index 191448c0e8..32b4b4f371 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -73,6 +73,7 @@ export default function relationships(state = initialState, action) { case ACCOUNT_UNMUTE_SUCCESS: case ACCOUNT_PIN_SUCCESS: case ACCOUNT_UNPIN_SUCCESS: + return normalizeRelationship(state, action.relationship); case RELATIONSHIPS_FETCH_SUCCESS: return normalizeRelationships(state, action.relationships); case submitAccountNote.fulfilled: diff --git a/app/javascript/mastodon/store/middlewares/errors.ts b/app/javascript/mastodon/store/middlewares/errors.ts index 4e720bfed4..9f28f5ff53 100644 --- a/app/javascript/mastodon/store/middlewares/errors.ts +++ b/app/javascript/mastodon/store/middlewares/errors.ts @@ -5,7 +5,7 @@ import { showAlertForError } from '../../actions/alerts'; const defaultFailSuffix = 'FAIL'; -export const errorsMiddleware: Middleware, RootState> = +export const errorsMiddleware: Middleware = ({ dispatch }) => (next) => (action: AnyAction & { skipAlert?: boolean; skipNotFound?: boolean }) => { diff --git a/app/javascript/mastodon/store/middlewares/loading_bar.ts b/app/javascript/mastodon/store/middlewares/loading_bar.ts index 379b3758a1..5fe8000731 100644 --- a/app/javascript/mastodon/store/middlewares/loading_bar.ts +++ b/app/javascript/mastodon/store/middlewares/loading_bar.ts @@ -15,7 +15,7 @@ const defaultTypeSuffixes: Config['promiseTypeSuffixes'] = [ export const loadingBarMiddleware = ( config: Config = {}, -): Middleware, RootState> => { +): Middleware => { const promiseTypeSuffixes = config.promiseTypeSuffixes ?? defaultTypeSuffixes; return ({ dispatch }) => diff --git a/app/javascript/mastodon/store/middlewares/sounds.ts b/app/javascript/mastodon/store/middlewares/sounds.ts index 092f403f5f..09ade7d753 100644 --- a/app/javascript/mastodon/store/middlewares/sounds.ts +++ b/app/javascript/mastodon/store/middlewares/sounds.ts @@ -34,10 +34,7 @@ const play = (audio: HTMLAudioElement) => { void audio.play(); }; -export const soundsMiddleware = (): Middleware< - Record, - RootState -> => { +export const soundsMiddleware = (): Middleware => { const soundCache: Record = {}; void ready(() => { diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts index d05a256bab..f1e71385a8 100644 --- a/app/javascript/mastodon/store/typed_functions.ts +++ b/app/javascript/mastodon/store/typed_functions.ts @@ -12,5 +12,4 @@ export const createAppAsyncThunk = createAsyncThunk.withTypes<{ state: RootState; dispatch: AppDispatch; rejectValue: string; - extra: { s: string; n: number }; }>(); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 51953858e0..9db7047791 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5274,6 +5274,11 @@ a.status-card { font-weight: 700; color: $primary-text-color; } + + span { + overflow: inherit; + text-overflow: inherit; + } } } } diff --git a/app/models/tag.rb b/app/models/tag.rb index 9ef271ceb4..4a9c289b37 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -20,6 +20,7 @@ # class Tag < ApplicationRecord + include Paginable has_and_belongs_to_many :statuses has_and_belongs_to_many :accounts diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 4db1be6f73..7f8792d918 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -125,6 +125,6 @@ class InitialStateSerializer < ActiveModel::Serializer end def sso_redirect - "/auth/auth/#{Devise.omniauth_providers[0]}" if ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1 + "/auth/auth/#{Devise.omniauth_providers[0]}" if ENV['ONE_CLICK_SSO_LOGIN'] == 'true' && ENV['OMNIAUTH_ONLY'] == 'true' && Devise.omniauth_providers.length == 1 end end diff --git a/app/serializers/rest/admin/tag_serializer.rb b/app/serializers/rest/admin/tag_serializer.rb index 425ba4ba34..54dbbe30ad 100644 --- a/app/serializers/rest/admin/tag_serializer.rb +++ b/app/serializers/rest/admin/tag_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::Admin::TagSerializer < REST::TagSerializer - attributes :id, :trendable, :usable, :requires_review + attributes :id, :trendable, :usable, :requires_review, :listable def id object.id.to_s diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index 59ac3bdea2..5b32ee49b3 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -19,6 +19,22 @@ media_host ||= host_to_url(ENV['AZURE_ALIAS_HOST']) media_host ||= host_to_url(ENV['S3_HOSTNAME']) if ENV['S3_ENABLED'] == 'true' media_host ||= assets_host +def sso_host + return unless ENV['ONE_CLICK_SSO_LOGIN'] == 'true' + return unless ENV['OMNIAUTH_ONLY'] == 'true' + return unless Devise.omniauth_providers.length == 1 + + provider = Devise.omniauth_configs[Devise.omniauth_providers[0]] + @sso_host ||= begin + # using CAS + provider.cas_url if ENV['CAS_ENABLED'] == 'true' + # using SAML + provider.options[:idp_sso_target_url] if ENV['SAML_ENABLED'] == 'true' + # or using OIDC + ENV['OIDC_AUTH_ENDPOINT'] || (OpenIDConnect::Discovery::Provider::Config.discover!(ENV['OIDC_ISSUER']).authorization_endpoint if ENV['OIDC_ENABLED'] == 'true') + end +end + Rails.application.config.content_security_policy do |p| p.base_uri :none p.default_src :none @@ -29,7 +45,13 @@ Rails.application.config.content_security_policy do |p| p.media_src :self, :https, :data, assets_host p.frame_src :self, :https p.manifest_src :self, assets_host - p.form_action :self + + if sso_host.present? + p.form_action :self, sso_host + else + p.form_action :self + end + p.child_src :self, :blob, assets_host p.worker_src :self, :blob, assets_host diff --git a/config/routes/api.rb b/config/routes/api.rb index b6b12d1534..8f8b7f5f81 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -314,6 +314,8 @@ namespace :api, format: false do post :test end end + + resources :tags, only: [:index, :show, :update] end end diff --git a/lib/mastodon/cli/search.rb b/lib/mastodon/cli/search.rb index 481e01d8e7..25a595aadd 100644 --- a/lib/mastodon/cli/search.rb +++ b/lib/mastodon/cli/search.rb @@ -16,7 +16,7 @@ module Mastodon::CLI option :concurrency, type: :numeric, default: 5, aliases: [:c], desc: 'Workload will be split between this number of threads' option :batch_size, type: :numeric, default: 100, aliases: [:b], desc: 'Number of records in each batch' - option :only, type: :array, enum: %w(instances accounts tags statuses), desc: 'Only process these indices' + option :only, type: :array, enum: %w(instances accounts tags statuses public_statuses), desc: 'Only process these indices' option :import, type: :boolean, default: true, desc: 'Import data from the database to the index' option :clean, type: :boolean, default: true, desc: 'Remove outdated documents from the index' option :reset_chewy, type: :boolean, default: false, desc: "Reset Chewy's internal index" diff --git a/spec/requests/api/v1/admin/tags_spec.rb b/spec/requests/api/v1/admin/tags_spec.rb new file mode 100644 index 0000000000..031be17f52 --- /dev/null +++ b/spec/requests/api/v1/admin/tags_spec.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Tags' do + let(:role) { UserRole.find_by(name: 'Admin') } + let(:user) { Fabricate(:user, role: role) } + let(:scopes) { 'admin:read admin:write' } + let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) } + let(:tag) { Fabricate(:tag) } + let(:headers) { { 'Authorization' => "Bearer #{token.token}" } } + + describe 'GET /api/v1/admin/tags' do + subject do + get '/api/v1/admin/tags', headers: headers, params: params + end + + let(:params) { {} } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + context 'when there are no tags' do + it 'returns an empty list' do + subject + + expect(body_as_json).to be_empty + end + end + + context 'when there are tagss' do + let!(:tags) do + [ + Fabricate(:tag), + Fabricate(:tag), + Fabricate(:tag), + Fabricate(:tag), + ] + end + + it 'returns the expected tags' do + subject + tags.each do |tag| + expect(body_as_json.find { |item| item[:id] == tag.id.to_s && item[:name] == tag.name }).to_not be_nil + end + end + + context 'with limit param' do + let(:params) { { limit: 2 } } + + it 'returns only the requested number of tags' do + subject + + expect(body_as_json.size).to eq(params[:limit]) + end + end + end + end + + describe 'GET /api/v1/admin/tags/:id' do + subject do + get "/api/v1/admin/tags/#{tag.id}", headers: headers + end + + let!(:tag) { Fabricate(:tag) } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns expected tag content' do + subject + + expect(body_as_json[:id].to_i).to eq(tag.id) + expect(body_as_json[:name]).to eq(tag.name) + end + + context 'when the requested tag does not exist' do + it 'returns http not found' do + get '/api/v1/admin/tags/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end + + describe 'PUT /api/v1/admin/tags/:id' do + subject do + put "/api/v1/admin/tags/#{tag.id}", headers: headers, params: params + end + + let!(:tag) { Fabricate(:tag) } + let(:params) { { display_name: tag.name.upcase } } + + it_behaves_like 'forbidden for wrong scope', 'write:statuses' + it_behaves_like 'forbidden for wrong scope', 'admin:read' + it_behaves_like 'forbidden for wrong role', '' + + it 'returns http success' do + subject + + expect(response).to have_http_status(200) + end + + it 'returns updated tag' do + subject + + expect(body_as_json[:id].to_i).to eq(tag.id) + expect(body_as_json[:name]).to eq(tag.name.upcase) + end + + context 'when the updated display name is invalid' do + let(:params) { { display_name: tag.name + tag.id.to_s } } + + it 'returns http unprocessable content' do + subject + + expect(response).to have_http_status(422) + end + end + + context 'when the requested tag does not exist' do + it 'returns http not found' do + get '/api/v1/admin/tags/-1', headers: headers + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/yarn.lock b/yarn.lock index 388acc230b..95bea8c933 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12379,9 +12379,9 @@ uuid@^8.3.2: integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== v8-compile-cache@^2.1.1: version "2.3.0"