diff --git a/.circleci/config.yml b/.circleci/config.yml index e8bfde299..2355d9d7d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -174,8 +174,7 @@ jobs: steps: - *attach_workspace - run: bundle exec i18n-tasks check-normalized - - run: bundle exec i18n-tasks unused - - run: bundle exec i18n-tasks missing -t plural + - run: bundle exec i18n-tasks unused -l en - run: bundle exec i18n-tasks check-consistent-interpolations workflows: diff --git a/CHANGELOG.md b/CHANGELOG.md index c89f35cdf..196a83d67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,31 @@ Changelog All notable changes to this project will be documented in this file. +## [2.9.1] - 2019-06-22 +### Added + +- Add moderation API ([Gargron](https://github.com/tootsuite/mastodon/pull/9387)) +- Add audio uploads ([Gargron](https://github.com/tootsuite/mastodon/pull/11123), [Gargron](https://github.com/tootsuite/mastodon/pull/11141)) + +### Changed + +- Change domain blocks to automatically support subdomains ([Gargron](https://github.com/tootsuite/mastodon/pull/11138)) +- Change Nanobox configuration to bring it up to date ([danhunsaker](https://github.com/tootsuite/mastodon/pull/11083)) + +### Removed + +- Remove expensive counters from federation page in admin UI ([Gargron](https://github.com/tootsuite/mastodon/pull/11139)) + +### Fixed + +- Fix converted media being saved with original extension and mime type ([Gargron](https://github.com/tootsuite/mastodon/pull/11130)) +- Fix layout of identity proofs settings ([acid-chicken](https://github.com/tootsuite/mastodon/pull/11126)) +- Fix active scope only returning suspended users ([ThibG](https://github.com/tootsuite/mastodon/pull/11111)) +- Fix sanitizer making block level elements unreadable ([Gargron](https://github.com/tootsuite/mastodon/pull/10836)) +- Fix label for site theme not being translated in admin UI ([palindromordnilap](https://github.com/tootsuite/mastodon/pull/11121)) +- Fix statuses not being filtered irreversibly in web UI under some circumstances ([ThibG](https://github.com/tootsuite/mastodon/pull/11113)) +- Fix scrolling behaviour in compose form ([ThibG](https://github.com/tootsuite/mastodon/pull/11093)) + ## [2.9.0] - 2019-06-13 ### Added diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f944309d5..76f512198 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,7 +18,9 @@ Bug reports and feature suggestions can be submitted to [GitHub Issues](https:// ## Translations -You can submit translations via pull request. +You can submit translations via [Crowdin](https://crowdin.com/project/mastodon). They are periodically merged into the codebase. + +[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin] ## Pull requests diff --git a/Gemfile b/Gemfile index 4fd981f75..b72f550eb 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.2' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.41', require: false +gem 'aws-sdk-s3', '~> 1.42', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -62,7 +62,7 @@ gem 'nokogiri', '~> 1.10' gem 'nsa', '~> 0.2' gem 'oj', '~> 3.7' gem 'ostatus2', '~> 2.0' -gem 'ox', '~> 2.10' +gem 'ox', '~> 2.11' gem 'posix-spawn', git: 'https://github.com/rtomayko/posix-spawn', ref: '58465d2e213991f8afb13b984854a49fcdcc980c' gem 'pundit', '~> 2.0' gem 'premailer-rails' @@ -108,14 +108,14 @@ group :production, :test do end group :test do - gem 'capybara', '~> 3.22' + gem 'capybara', '~> 3.24' gem 'climate_control', '~> 0.2' gem 'faker', '~> 1.9' gem 'microformats', '~> 4.1' gem 'rails-controller-testing', '~> 1.0' gem 'rspec-sidekiq', '~> 3.0' gem 'simplecov', '~> 0.16', require: false - gem 'webmock', '~> 3.5' + gem 'webmock', '~> 3.6' gem 'parallel_tests', '~> 2.29' end diff --git a/Gemfile.lock b/Gemfile.lock index d8a68d8c9..43d98bbdc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,8 +76,8 @@ GEM av (0.9.0) cocaine (~> 0.5.3) aws-eventstream (1.0.3) - aws-partitions (1.169.0) - aws-sdk-core (3.54.0) + aws-partitions (1.175.0) + aws-sdk-core (3.55.0) aws-eventstream (~> 1.0, >= 1.0.2) aws-partitions (~> 1.0) aws-sigv4 (~> 1.1) @@ -85,7 +85,7 @@ GEM aws-sdk-kms (1.21.0) aws-sdk-core (~> 3, >= 3.53.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.41.0) + aws-sdk-s3 (1.42.0) aws-sdk-core (~> 3, >= 3.53.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -129,7 +129,7 @@ GEM sshkit (~> 1.3) capistrano-yarn (2.0.2) capistrano (~> 3.0) - capybara (3.22.0) + capybara (3.24.0) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) @@ -253,7 +253,7 @@ GEM railties (>= 4.0.1) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashdiff (0.3.7) + hashdiff (0.4.0) hashie (3.6.0) heapy (0.1.4) highline (2.0.1) @@ -269,7 +269,7 @@ GEM domain_name (~> 0.5) http-form_data (2.1.1) http_accept_language (2.1.1) - httplog (1.3.0) + httplog (1.3.1) rack (>= 1.0) rainbow (>= 2.0.0) i18n (1.6.0) @@ -320,7 +320,7 @@ GEM letter_opener (~> 1.0) railties (>= 3.2) link_header (0.0.8) - lograge (0.11.1) + lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) railties (>= 4) @@ -382,7 +382,7 @@ GEM addressable (~> 2.5) http (~> 3.0) nokogiri (~> 1.8) - ox (2.10.1) + ox (2.11.0) paperclip (6.0.0) activemodel (>= 4.2.0) activesupport (>= 4.2.0) @@ -401,7 +401,7 @@ GEM equatable (~> 0.5.0) tty-color (~> 0.4.0) pg (1.1.4) - pghero (2.2.0) + pghero (2.2.1) activerecord pkg-config (1.3.7) premailer (1.11.1) @@ -531,15 +531,15 @@ GEM rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.0.0) - rack (>= 2.0) + rubocop-rails (2.0.1) + rack (>= 1.1) rubocop (>= 0.70.0) ruby-progressbar (1.10.1) ruby-saml (1.9.0) nokogiri (>= 1.5.10) rufus-scheduler (3.5.2) fugit (~> 1.1, >= 1.1.5) - safe_yaml (1.0.4) + safe_yaml (1.0.5) sanitize (5.0.0) crass (~> 1.0.2) nokogiri (>= 1.8.0) @@ -621,10 +621,10 @@ GEM uniform_notifier (1.12.1) warden (1.2.8) rack (>= 2.0.6) - webmock (3.5.1) + webmock (3.6.0) addressable (>= 2.3.6) crack (>= 0.3.2) - hashdiff + hashdiff (>= 0.4.0, < 2.0.0) webpacker (4.0.7) activesupport (>= 4.2) rack-proxy (>= 0.6.1) @@ -647,7 +647,7 @@ DEPENDENCIES active_record_query_trace (~> 1.6) addressable (~> 2.6) annotate (~> 2.7) - aws-sdk-s3 (~> 1.41) + aws-sdk-s3 (~> 1.42) better_errors (~> 2.5) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -660,7 +660,7 @@ DEPENDENCIES capistrano-rails (~> 1.4) capistrano-rbenv (~> 2.1) capistrano-yarn (~> 2.0) - capybara (~> 3.22) + capybara (~> 3.24) charlock_holmes (~> 0.7.6) chewy (~> 5.0) cld3 (~> 3.2.4) @@ -710,7 +710,7 @@ DEPENDENCIES omniauth-cas (~> 1.1) omniauth-saml (~> 1.10) ostatus2 (~> 2.0) - ox (~> 2.10) + ox (~> 2.11) paperclip (~> 6.0) paperclip-av-transcoder (~> 0.6) parallel_tests (~> 2.29) @@ -757,7 +757,7 @@ DEPENDENCIES tty-prompt (~> 0.19) twitter-text (~> 1.14) tzinfo-data (~> 1.2019) - webmock (~> 3.5) + webmock (~> 3.6) webpacker (~> 4.0) webpush diff --git a/app/controllers/admin/accounts_controller.rb b/app/controllers/admin/accounts_controller.rb index b0d45ce47..0c7760d77 100644 --- a/app/controllers/admin/accounts_controller.rb +++ b/app/controllers/admin/accounts_controller.rb @@ -127,6 +127,7 @@ module Admin :by_domain, :active, :pending, + :disabled, :silenced, :suspended, :username, diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb index 71597763b..377cac8ad 100644 --- a/app/controllers/admin/domain_blocks_controller.rb +++ b/app/controllers/admin/domain_blocks_controller.rb @@ -13,7 +13,7 @@ module Admin authorize :domain_block, :create? @domain_block = DomainBlock.new(resource_params) - existing_domain_block = resource_params[:domain].present? ? DomainBlock.find_by(domain: resource_params[:domain]) : nil + existing_domain_block = resource_params[:domain].present? ? DomainBlock.rule_for(resource_params[:domain]) : nil if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block) @domain_block.save diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 6dd659a30..7888e844f 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -18,7 +18,7 @@ module Admin @blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count @available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url) @media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size) - @domain_block = DomainBlock.find_by(domain: params[:id]) + @domain_block = DomainBlock.rule_for(params[:id]) end private diff --git a/app/controllers/api/v1/admin/account_actions_controller.rb b/app/controllers/api/v1/admin/account_actions_controller.rb new file mode 100644 index 000000000..29c9b7107 --- /dev/null +++ b/app/controllers/api/v1/admin/account_actions_controller.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountActionsController < Api::BaseController + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' } + before_action :require_staff! + before_action :set_account + + def create + account_action = Admin::AccountAction.new(resource_params) + account_action.target_account = @account + account_action.current_account = current_account + account_action.save! + + render_empty + end + + private + + def set_account + @account = Account.find(params[:account_id]) + end + + def resource_params + params.permit( + :type, + :report_id, + :warning_preset_id, + :text, + :send_email_notification + ) + end +end diff --git a/app/controllers/api/v1/admin/accounts_controller.rb b/app/controllers/api/v1/admin/accounts_controller.rb new file mode 100644 index 000000000..c306180ca --- /dev/null +++ b/app/controllers/api/v1/admin/accounts_controller.rb @@ -0,0 +1,128 @@ +# frozen_string_literal: true + +class Api::V1::Admin::AccountsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:accounts' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:accounts' }, except: [:index, :show] + before_action :require_staff! + before_action :set_accounts, only: :index + before_action :set_account, except: :index + before_action :require_local_account!, only: [:enable, :approve, :reject] + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + local + remote + by_domain + active + pending + disabled + silenced + suspended + username + display_name + email + ip + staff + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :account, :index? + render json: @accounts, each_serializer: REST::Admin::AccountSerializer + end + + def show + authorize @account, :show? + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def enable + authorize @account.user, :enable? + @account.user.enable! + log_action :enable, @account.user + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def approve + authorize @account.user, :approve? + @account.user.approve! + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def reject + authorize @account.user, :reject? + SuspendAccountService.new.call(@account, including_user: true, destroy: true, skip_distribution: true) + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsilence + authorize @account, :unsilence? + @account.unsilence! + log_action :unsilence, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + def unsuspend + authorize @account, :unsuspend? + @account.unsuspend! + log_action :unsuspend, @account + render json: @account, serializer: REST::Admin::AccountSerializer + end + + private + + def set_accounts + @accounts = filtered_accounts.order(id: :desc).includes(user: [:invite_request, :invite]).paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_account + @account = Account.find(params[:id]) + end + + def filtered_accounts + AccountFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_accounts_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_accounts_url(pagination_params(min_id: pagination_since_id)) unless @accounts.empty? + end + + def pagination_max_id + @accounts.last.id + end + + def pagination_since_id + @accounts.first.id + end + + def records_continue? + @accounts.size == limit_param(LIMIT) + end + + def pagination_params(core_params) + params.slice(*PAGINATION_PARAMS).permit(*PAGINATION_PARAMS).merge(core_params) + end + + def require_local_account! + forbidden unless @account.local? && @account.user.present? + end +end diff --git a/app/controllers/api/v1/admin/reports_controller.rb b/app/controllers/api/v1/admin/reports_controller.rb new file mode 100644 index 000000000..1d48d3160 --- /dev/null +++ b/app/controllers/api/v1/admin/reports_controller.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +class Api::V1::Admin::ReportsController < Api::BaseController + include Authorization + include AccountableConcern + + LIMIT = 100 + + before_action -> { doorkeeper_authorize! :'admin:read', :'admin:read:reports' }, only: [:index, :show] + before_action -> { doorkeeper_authorize! :'admin:write', :'admin:write:reports' }, except: [:index, :show] + before_action :require_staff! + before_action :set_reports, only: :index + before_action :set_report, except: :index + + after_action :insert_pagination_headers, only: :index + + FILTER_PARAMS = %i( + resolved + account_id + target_account_id + ).freeze + + PAGINATION_PARAMS = (%i(limit) + FILTER_PARAMS).freeze + + def index + authorize :report, :index? + render json: @reports, each_serializer: REST::Admin::ReportSerializer + end + + def show + authorize @report, :show? + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def assign_to_self + authorize @report, :update? + @report.update!(assigned_account_id: current_account.id) + log_action :assigned_to_self, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def unassign + authorize @report, :update? + @report.update!(assigned_account_id: nil) + log_action :unassigned, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def reopen + authorize @report, :update? + @report.unresolve! + log_action :reopen, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + def resolve + authorize @report, :update? + @report.resolve!(current_account) + log_action :resolve, @report + render json: @report, serializer: REST::Admin::ReportSerializer + end + + private + + def set_reports + @reports = filtered_reports.order(id: :desc).with_accounts.paginate_by_id(limit_param(LIMIT), params_slice(:max_id, :since_id, :min_id)) + end + + def set_report + @report = Report.find(params[:id]) + end + + def filtered_reports + ReportFilter.new(filter_params).results + end + + def filter_params + params.permit(*FILTER_PARAMS) + end + + def insert_pagination_headers + set_pagination_headers(next_path, prev_path) + end + + def next_path + api_v1_admin_reports_url(pagination_params(max_id: pagination_max_id)) if records_continue? + end + + def prev_path + api_v1_admin_reports_url(pagination_params(min_id: pagination_since_id)) unless @reports.empty? + end + + def pagination_max_id + @reports.last.id + end + + def pagination_since_id + @reports.first.id + end + + def records_continue? + @reports.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/media_controller.rb b/app/controllers/media_controller.rb index a245db2d1..d44b52d26 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -7,6 +7,8 @@ class MediaController < ApplicationController before_action :set_media_attachment before_action :verify_permitted_status! + before_action :check_playable, only: :player + before_action :allow_iframing, only: :player content_security_policy only: :player do |p| p.frame_ancestors(false) @@ -18,8 +20,6 @@ class MediaController < ApplicationController def player @body_classes = 'player' - response.headers['X-Frame-Options'] = 'ALLOWALL' - raise ActiveRecord::RecordNotFound unless @media_attachment.video? || @media_attachment.gifv? end private @@ -34,4 +34,12 @@ class MediaController < ApplicationController # Reraise in order to get a 404 instead of a 403 error code raise ActiveRecord::RecordNotFound end + + def check_playable + not_found unless @media_attachment.larger_media_format? + end + + def allow_iframing + response.headers['X-Frame-Options'] = 'ALLOWALL' + end end diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 950cf6d09..8fc18dd06 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -39,6 +39,6 @@ class MediaProxyController < ApplicationController end def reject_media? - DomainBlock.find_by(domain: @media_attachment.account.domain)&.reject_media? + DomainBlock.reject_media?(@media_attachment.account.domain) end end diff --git a/app/controllers/settings/identity_proofs_controller.rb b/app/controllers/settings/identity_proofs_controller.rb index e22b4d9be..a749d8020 100644 --- a/app/controllers/settings/identity_proofs_controller.rb +++ b/app/controllers/settings/identity_proofs_controller.rb @@ -56,8 +56,4 @@ class Settings::IdentityProofsController < Settings::BaseController def post_params params.require(:account_identity_proof).permit(:post_status, :status_text) end - - def set_body_classes - @body_classes = '' - end end diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index b0861fc6b..88788eec9 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -48,9 +48,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; if (notification.type === 'mention') { + const dropRegex = regexFromFilters(filters.filter(filter => filter.get('irreversible'))); const regex = regexFromFilters(filters); const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content); + if (dropRegex && dropRegex.test(searchIndex)) { + return; + } + filtered = regex && regex.test(searchIndex); } diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 56618462b..77bac61ee 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -157,7 +157,7 @@ class Item extends React.PureComponent { if (attachment.get('type') === 'unknown') { return (
- +
diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index aa5e870dc..9b1035649 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -333,17 +333,17 @@ class Status extends ImmutablePureComponent { media={status.get('media_attachments')} /> ); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') { - const video = status.getIn(['media_attachments', 0]); + } else if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) { + const attachment = status.getIn(['media_attachments', 0]); media = ( {Component => ( { - if (this.composeForm) { + if (this.composeForm && !this.props.singleColumn) { this.composeForm.scrollIntoView(); } } diff --git a/app/javascript/mastodon/features/compose/components/upload_button.js b/app/javascript/mastodon/features/compose/components/upload_button.js index 90e2769f3..e141857be 100644 --- a/app/javascript/mastodon/features/compose/components/upload_button.js +++ b/app/javascript/mastodon/features/compose/components/upload_button.js @@ -7,9 +7,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePropTypes from 'react-immutable-proptypes'; const messages = defineMessages({ - upload: { id: 'upload_button.label', defaultMessage: 'Add media (JPEG, PNG, GIF, WebM, MP4, MOV)' }, + upload: { id: 'upload_button.label', defaultMessage: 'Add media ({formats})' }, }); +const SUPPORTED_FORMATS = 'JPEG, PNG, GIF, WebM, MP4, MOV, OGG, WAV, MP3, FLAC'; + const makeMapStateToProps = () => { const mapStateToProps = state => ({ acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), @@ -60,9 +62,9 @@ class UploadButton extends ImmutablePureComponent { return (
- +