diff --git a/.circleci/config.yml b/.circleci/config.yml index dd943e32795..9f43a057356 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,12 +5,13 @@ aliases: docker: - image: circleci/ruby:2.7-buster-node environment: &ruby_environment + BUNDLE_JOBS: 3 + BUNDLE_RETRY: 3 BUNDLE_APP_CONFIG: ./.bundle/ BUNDLE_PATH: ./vendor/bundle/ DB_HOST: localhost DB_USER: root RAILS_ENV: test - PARALLEL_TEST_PROCESSORS: 4 ALLOW_NOPAM: true CONTINUOUS_INTEGRATION: true DISABLE_SIMPLECOV: true @@ -32,9 +33,9 @@ aliases: - &restore_ruby_dependencies restore_cache: keys: - - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} - - v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}- - - v2-ruby-dependencies- + - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} + - v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}- + - v3-ruby-dependencies- - &install_steps steps: @@ -42,11 +43,13 @@ aliases: - *attach_workspace - restore_cache: keys: - - v1-node-dependencies-{{ checksum "yarn.lock" }} - - v1-node-dependencies- - - run: yarn install --frozen-lockfile + - v2-node-dependencies-{{ checksum "yarn.lock" }} + - v2-node-dependencies- + - run: + name: Install yarn dependencies + command: yarn install --frozen-lockfile - save_cache: - key: v1-node-dependencies-{{ checksum "yarn.lock" }} + key: v2-node-dependencies-{{ checksum "yarn.lock" }} paths: - ./node_modules/ - *persist_to_workspace @@ -57,27 +60,28 @@ aliases: command: | sudo apt-get update sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler - - ## TODO: FIX THESE BUSTER DEPENDANCES - sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb - sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb - sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb - sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb - &install_ruby_dependencies steps: - *attach_workspace - *install_system_dependencies - - run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version + - run: + name: Set Ruby version + command: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version - *restore_ruby_dependencies - - run: bundle config set clean 'true' - - run: bundle config set deployment 'true' - - run: bundle config set with 'pam_authentication' - - run: bundle config set without 'development production' - - run: bundle config set frozen 'true' - - run: bundle install --jobs 16 --retry 3 && bundle clean + - run: + name: Set bundler settings + command: | + bundle config clean 'true' + bundle config deployment 'true' + bundle config with 'pam_authentication' + bundle config without 'development production' + bundle config frozen 'true' + - run: + name: Install bundler dependencies + command: bundle check || (bundle install && bundle clean) - save_cache: - key: v2-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} + key: v3-ruby-dependencies-{{ checksum "/tmp/.ruby-version" }}-{{ checksum "Gemfile.lock" }} paths: - ./.bundle/ - ./vendor/bundle/ @@ -88,17 +92,26 @@ aliases: - ./mastodon/vendor/bundle/ - &test_steps + parallelism: 4 steps: - *attach_workspace - *install_system_dependencies - - run: sudo apt-get install -y ffmpeg - run: - name: Prepare Tests - command: ./bin/rails parallel:create parallel:load_schema parallel:prepare + name: Install FFMPEG + command: sudo apt-get install -y ffmpeg - run: - name: Run Tests - command: ./bin/retry bundle exec parallel_test ./spec/ --group-by filesize --type rspec - + name: Load database schema + command: ./bin/rails db:create db:schema:load db:seed + - run: + name: Run rspec in parallel + command: | + bundle exec rspec --profile 10 \ + --format RspecJunitFormatter \ + --out test_results/rspec.xml \ + --format progress \ + $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) + - store_test_results: + path: test_results jobs: install: <<: *defaults @@ -115,19 +128,14 @@ jobs: environment: *ruby_environment <<: *install_ruby_dependencies - install-ruby2.5: - <<: *defaults - docker: - - image: circleci/ruby:2.5-buster-node - environment: *ruby_environment - <<: *install_ruby_dependencies - build: <<: *defaults steps: - *attach_workspace - *install_system_dependencies - - run: ./bin/rails assets:precompile + - run: + name: Precompile assets + command: ./bin/rails assets:precompile - persist_to_workspace: root: ~/projects/ paths: @@ -149,10 +157,10 @@ jobs: - *install_system_dependencies - run: name: Create database - command: ./bin/rails parallel:create + command: ./bin/rails db:create - run: name: Run migrations - command: ./bin/rails parallel:migrate + command: ./bin/rails db:migrate test-ruby2.7: <<: *defaults @@ -178,35 +186,33 @@ jobs: - image: circleci/redis:5-alpine <<: *test_steps - test-ruby2.5: - <<: *defaults - docker: - - image: circleci/ruby:2.5-buster-node - environment: *ruby_environment - - image: circleci/postgres:12.2 - environment: - POSTGRES_USER: root - POSTGRES_HOST_AUTH_METHOD: trust - - image: circleci/redis:5-alpine - <<: *test_steps - test-webui: <<: *defaults docker: - image: circleci/node:12-buster steps: - *attach_workspace - - run: ./bin/retry yarn test:jest + - run: + name: Run jest + command: yarn test:jest check-i18n: <<: *defaults steps: - *attach_workspace - *install_system_dependencies - - run: bundle exec i18n-tasks check-normalized - - run: bundle exec i18n-tasks unused -l en - - run: bundle exec i18n-tasks check-consistent-interpolations - - run: bundle exec rake repo:check_locales_files + - run: + name: Check locale file normalization + command: bundle exec i18n-tasks check-normalized + - run: + name: Check for unused strings + command: bundle exec i18n-tasks unused -l en + - run: + name: Check for wrong string interpolations + command: bundle exec i18n-tasks check-consistent-interpolations + - run: + name: Check that all required locale files exist + command: bundle exec rake repo:check_locales_files workflows: version: 2 @@ -220,10 +226,6 @@ workflows: requires: - install - install-ruby2.7 - - install-ruby2.5: - requires: - - install - - install-ruby2.7 - build: requires: - install-ruby2.7 @@ -238,10 +240,6 @@ workflows: requires: - install-ruby2.6 - build - - test-ruby2.5: - requires: - - install-ruby2.5 - - build - test-webui: requires: - install diff --git a/Gemfile b/Gemfile index d59aa9d1226..c5b95711456 100644 --- a/Gemfile +++ b/Gemfile @@ -20,7 +20,7 @@ gem 'makara', '~> 0.4' gem 'pghero', '~> 2.4' gem 'dotenv-rails', '~> 2.7' -gem 'aws-sdk-s3', '~> 1.61', require: false +gem 'aws-sdk-s3', '~> 1.63', require: false gem 'fog-core', '<= 2.1.0' gem 'fog-openstack', '~> 0.3', require: false gem 'paperclip', '~> 6.0' @@ -129,6 +129,7 @@ group :test do gem 'simplecov', '~> 0.18', require: false gem 'webmock', '~> 3.8' gem 'parallel_tests', '~> 2.32' + gem 'rspec_junit_formatter', '~> 0.4' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 13f6bed594b..7525911ffbc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -92,7 +92,7 @@ GEM av (0.9.0) cocaine (~> 0.5.3) aws-eventstream (1.1.0) - aws-partitions (1.296.0) + aws-partitions (1.303.0) aws-sdk-core (3.94.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) @@ -101,7 +101,7 @@ GEM aws-sdk-kms (1.30.0) aws-sdk-core (~> 3, >= 3.71.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.61.2) + aws-sdk-s3 (1.63.0) aws-sdk-core (~> 3, >= 3.83.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) @@ -277,7 +277,7 @@ GEM http-parser (~> 1.2.0) http-cookie (1.0.3) domain_name (~> 0.5) - http-form_data (2.2.0) + http-form_data (2.3.0) http-parser (1.2.1) ffi-compiler (>= 1.0, < 2.0) http_accept_language (2.1.1) @@ -303,7 +303,7 @@ GEM jmespath (1.4.0) json (2.3.0) json-canonicalization (0.2.0) - json-ld (3.1.2) + json-ld (3.1.3) htmlentities (~> 4.3) json-canonicalization (~> 0.2) link_header (~> 0.0, >= 0.0.8) @@ -359,7 +359,7 @@ GEM nokogiri (~> 1.10) mime-types (3.3.1) mime-types-data (~> 3.2015) - mime-types-data (3.2019.1009) + mime-types-data (3.2020.0425) mimemagic (0.3.4) mini_mime (1.0.2) mini_portile2 (2.4.0) @@ -382,7 +382,7 @@ GEM concurrent-ruby (~> 1.0, >= 1.0.2) sidekiq (>= 3.5) statsd-ruby (~> 1.4, >= 1.4.0) - oj (3.10.5) + oj (3.10.6) omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) @@ -409,7 +409,7 @@ GEM parallel parser (2.7.1.1) ast (~> 2.4.0) - parslet (1.8.2) + parslet (2.0.0) pastel (0.7.3) equatable (~> 0.6) tty-color (~> 0.5) @@ -542,6 +542,8 @@ GEM rspec-core (~> 3.0, >= 3.0.0) sidekiq (>= 2.4.0) rspec-support (3.9.2) + rspec_junit_formatter (0.4.1) + rspec-core (>= 2, < 4, != 2.12.0) rubocop (0.79.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -554,7 +556,7 @@ GEM rack (>= 1.1) rubocop (>= 0.72.0) ruby-progressbar (1.10.1) - ruby-saml (1.9.0) + ruby-saml (1.11.0) nokogiri (>= 1.5.10) rufus-scheduler (3.6.0) fugit (~> 1.1, >= 1.1.6) @@ -668,7 +670,7 @@ DEPENDENCIES active_record_query_trace (~> 1.7) addressable (~> 2.7) annotate (~> 3.1) - aws-sdk-s3 (~> 1.61) + aws-sdk-s3 (~> 1.63) better_errors (~> 2.6) binding_of_caller (~> 0.7) blurhash (~> 0.1) @@ -765,6 +767,7 @@ DEPENDENCIES rqrcode (~> 1.1) rspec-rails (~> 4.0) rspec-sidekiq (~> 3.0) + rspec_junit_formatter (~> 0.4) rubocop (~> 0.79) rubocop-rails (~> 2.5) ruby-progressbar (~> 1.10) diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index ee48da17756..52d09cff8b8 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -28,7 +28,7 @@ class AccountsController < ApplicationController end @pinned_statuses = cache_collection(@account.pinned_statuses, Status) if show_pinned_statuses? - @statuses = filtered_status_page(params) + @statuses = filtered_status_page @statuses = cache_collection(@statuses, Status) @rss_url = rss_url @@ -141,12 +141,12 @@ class AccountsController < ApplicationController request.path.split('.').first.ends_with?(Addressable::URI.parse("/tagged/#{params[:tag]}").normalize) end - def filtered_status_page(params) - if params[:min_id].present? - filtered_statuses.paginate_by_min_id(PAGE_SIZE, params[:min_id]).reverse - else - filtered_statuses.paginate_by_max_id(PAGE_SIZE, params[:max_id], params[:since_id]).to_a - end + def filtered_status_page + filtered_statuses.paginate_by_id(PAGE_SIZE, params_slice(:max_id, :min_id, :since_id)) + end + + def params_slice(*keys) + params.slice(*keys).permit(*keys) end def restrict_fields_to diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index 910fefb1c7a..c1e7aa55023 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -24,20 +24,23 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController def set_size case params[:id] when 'featured' - @account.pinned_statuses.count + @size = @account.pinned_statuses.count else - raise ActiveRecord::RecordNotFound + not_found end end def scope_for_collection case params[:id] when 'featured' - return Status.none if @account.blocking?(signed_request_account) - - @account.pinned_statuses - else - raise ActiveRecord::RecordNotFound + # Because in public fetch mode we cache the response, there would be no + # benefit from performing the check below, since a blocked account or domain + # would likely be served the cache from the reverse proxy anyway + if authorized_fetch_mode? && !signed_request_account.nil? && (@account.blocking?(signed_request_account) || (!signed_request_account.domain.nil? && @account.domain_blocking?(signed_request_account.domain))) + Status.none + else + @account.pinned_statuses + end end end diff --git a/app/controllers/activitypub/outboxes_controller.rb b/app/controllers/activitypub/outboxes_controller.rb index 891756b7e6a..e25a4bc0797 100644 --- a/app/controllers/activitypub/outboxes_controller.rb +++ b/app/controllers/activitypub/outboxes_controller.rb @@ -11,7 +11,7 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController before_action :set_cache_headers def show - expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?)) render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json' end @@ -50,12 +50,12 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController return unless page_requested? @statuses = @account.statuses.permitted_for(@account, signed_request_account) - @statuses = params[:min_id].present? ? @statuses.paginate_by_min_id(LIMIT, params[:min_id]).reverse : @statuses.paginate_by_max_id(LIMIT, params[:max_id]) + @statuses = @statuses.paginate_by_id(LIMIT, params_slice(:max_id, :min_id, :since_id)) @statuses = cache_collection(@statuses, Status) end def page_requested? - params[:page] == 'true' + truthy_param?(:page) end def page_params diff --git a/app/controllers/activitypub/replies_controller.rb b/app/controllers/activitypub/replies_controller.rb index c62061555bb..43bf4e657d1 100644 --- a/app/controllers/activitypub/replies_controller.rb +++ b/app/controllers/activitypub/replies_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ActivityPub::RepliesController < ActivityPub::BaseController - include SignatureAuthentication + include SignatureVerification include Authorization include AccountOwnedConcern @@ -19,15 +19,19 @@ class ActivityPub::RepliesController < ActivityPub::BaseController private + def pundit_user + signed_request_account + end + def set_status @status = @account.statuses.find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def set_replies - @replies = page_params[:only_other_accounts] ? Status.where.not(account_id: @account.id) : @account.statuses + @replies = only_other_accounts? ? Status.where.not(account_id: @account.id) : @account.statuses @replies = @replies.where(in_reply_to_id: @status.id, visibility: [:public, :unlisted]) @replies = @replies.paginate_by_min_id(DESCENDANTS_LIMIT, params[:min_id]) end @@ -38,7 +42,7 @@ class ActivityPub::RepliesController < ActivityPub::BaseController type: :unordered, part_of: account_status_replies_url(@account, @status), next: next_page, - items: @replies.map { |status| status.local ? status : status.uri } + items: @replies.map { |status| status.local? ? status : status.uri } ) return page if page_requested? @@ -51,16 +55,21 @@ class ActivityPub::RepliesController < ActivityPub::BaseController end def page_requested? - params[:page] == 'true' + truthy_param?(:page) + end + + def only_other_accounts? + truthy_param?(:only_other_accounts) end def next_page only_other_accounts = !(@replies&.last&.account_id == @account.id && @replies.size == DESCENDANTS_LIMIT) + account_status_replies_url( @account, @status, page: true, - min_id: only_other_accounts && !page_params[:only_other_accounts] ? nil : @replies&.last&.id, + min_id: only_other_accounts && !only_other_accounts? ? nil : @replies&.last&.id, only_other_accounts: only_other_accounts ) end diff --git a/app/controllers/api/v1/polls/votes_controller.rb b/app/controllers/api/v1/polls/votes_controller.rb index e1d26106afb..513b937ef2d 100644 --- a/app/controllers/api/v1/polls/votes_controller.rb +++ b/app/controllers/api/v1/polls/votes_controller.rb @@ -18,7 +18,7 @@ class Api::V1::Polls::VotesController < Api::BaseController @poll = Poll.attached.find(params[:poll_id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def vote_params diff --git a/app/controllers/api/v1/polls_controller.rb b/app/controllers/api/v1/polls_controller.rb index 744baf7bb42..6435e9f0dcd 100644 --- a/app/controllers/api/v1/polls_controller.rb +++ b/app/controllers/api/v1/polls_controller.rb @@ -17,7 +17,7 @@ class Api::V1::PollsController < Api::BaseController @poll = Poll.attached.find(params[:id]) authorize @poll.status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def refresh_poll diff --git a/app/controllers/api/v1/push/subscriptions_controller.rb b/app/controllers/api/v1/push/subscriptions_controller.rb index 1cbc92b93d7..d34b333eb3a 100644 --- a/app/controllers/api/v1/push/subscriptions_controller.rb +++ b/app/controllers/api/v1/push/subscriptions_controller.rb @@ -4,6 +4,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController before_action -> { doorkeeper_authorize! :push } before_action :require_user! before_action :set_web_push_subscription + before_action :check_web_push_subscription, only: [:show, :update] def create @web_subscription&.destroy! @@ -21,16 +22,11 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController end def show - raise ActiveRecord::RecordNotFound if @web_subscription.nil? - render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer end def update - raise ActiveRecord::RecordNotFound if @web_subscription.nil? - @web_subscription.update!(data: data_params) - render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer end @@ -45,12 +41,17 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController @web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id) end + def check_web_push_subscription + not_found if @web_subscription.nil? + end + def subscription_params params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh]) end def data_params return {} if params[:data].blank? + params.require(:data).permit(alerts: [:follow, :follow_request, :favourite, :reblog, :mention, :poll]) end end diff --git a/app/controllers/api/v1/statuses/mutes_controller.rb b/app/controllers/api/v1/statuses/mutes_controller.rb index 43c7a525ad6..87071a2b9aa 100644 --- a/app/controllers/api/v1/statuses/mutes_controller.rb +++ b/app/controllers/api/v1/statuses/mutes_controller.rb @@ -28,8 +28,7 @@ class Api::V1::Statuses::MutesController < Api::BaseController @status = Status.find(params[:status_id]) authorize @status, :show? rescue Mastodon::NotPermittedError - # Reraise in order to get a 404 instead of a 403 error code - raise ActiveRecord::RecordNotFound + not_found end def set_conversation diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 29ae9176266..b3edce67608 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -68,7 +68,7 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def set_thread diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 05cf09c28af..1d166d6e73b 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -33,7 +33,7 @@ class MediaController < ApplicationController def verify_permitted_status! authorize @media_attachment.status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def check_playable diff --git a/app/controllers/remote_interaction_controller.rb b/app/controllers/remote_interaction_controller.rb index e058d0ed5b5..51bb9bdeaa7 100644 --- a/app/controllers/remote_interaction_controller.rb +++ b/app/controllers/remote_interaction_controller.rb @@ -42,7 +42,7 @@ class RemoteInteractionController < ApplicationController @status = Status.find(params[:id]) authorize @status, :show? rescue Mastodon::NotPermittedError - raise ActiveRecord::RecordNotFound + not_found end def set_body_classes diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 588063d0103..a1b7f432041 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -49,7 +49,7 @@ class StatusesController < ApplicationController def embed use_pack 'embed' - return not_found if @status.hidden? + return not_found if @status.hidden? || @status.reblog? expires_in 180, public: true response.headers['X-Frame-Options'] = 'ALLOWALL' diff --git a/app/javascript/core/settings.js b/app/javascript/core/settings.js index e02c91cc764..9fe03f90c2c 100644 --- a/app/javascript/core/settings.js +++ b/app/javascript/core/settings.js @@ -10,7 +10,7 @@ delegate(document, '#account_display_name', 'input', ({ target }) => { if (target.value) { name.innerHTML = emojify(escapeTextContentForBrowser(target.value)); } else { - name.textContent = document.querySelector('#default_account_display_name').textContent; + name.textContent = name.textContent = target.dataset.default; } } }); diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index d9c75e063fd..50e36531e51 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -55,7 +55,7 @@ export function updateTimeline(timeline, status, accept) { export function deleteFromTimelines(id) { return (dispatch, getState) => { const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); dispatch({ diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index b3da4fc2f76..60ed859a3f4 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { - this.focusedItem.focus(); + this.focusedItem.focus({ preventScroll: true }); } this.setState({ mounted: true }); } diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 84c040a86b2..bee06e64c7f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -64,9 +64,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, withPassive); if (this.focusedItem) { - this.focusedItem.focus(); + this.focusedItem.focus({ preventScroll: true }); } else { - this.node.firstChild.focus(); + this.node.firstChild.focus({ preventScroll: true }); } this.setState({ mounted: true }); } diff --git a/app/javascript/flavours/glitch/packs/public.js b/app/javascript/flavours/glitch/packs/public.js index e5a567205d6..58febcf5b18 100644 --- a/app/javascript/flavours/glitch/packs/public.js +++ b/app/javascript/flavours/glitch/packs/public.js @@ -99,15 +99,13 @@ function main() { delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); delegate(document, '.status__content__spoiler-link', 'click', function() { - const contentEl = this.parentNode.parentNode.querySelector('.e-content'); + const statusEl = this.parentNode.parentNode; - if (contentEl.style.display === 'block') { - contentEl.style.display = 'none'; - this.parentNode.style.marginBottom = 0; + if (statusEl.dataset.spoiler === 'expanded') { + statusEl.dataset.spoiler = 'folded'; this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format(); } else { - contentEl.style.display = 'block'; - this.parentNode.style.marginBottom = null; + statusEl.dataset.spoiler = 'expanded'; this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format(); } @@ -115,8 +113,8 @@ function main() { }); [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => { - const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content'); - const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); + const statusEl = spoilerLink.parentNode.parentNode; + const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format(); }); }); diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 8926e49f1cf..5db766b9672 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -24,7 +24,7 @@ const importStatuses = (state, statuses) => const deleteStatus = (state, id, references) => { references.forEach(ref => { - state = deleteStatus(state, ref[0], []); + state = deleteStatus(state, ref, []); }); return state.delete(id); diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index be7b2441b1e..882b4879003 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -94,7 +94,7 @@ const updateTimeline = (state, timeline, status, usePendingItems, filtered) => { })); }; -const deleteStatus = (state, id, accountId, references, exclude_account = null) => { +const deleteStatus = (state, id, references, exclude_account = null) => { state.keySeq().forEach(timeline => { if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { const helper = list => list.filterNot(item => item === id); @@ -104,7 +104,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null) // Remove reblogs of deleted status references.forEach(ref => { - state = deleteStatus(state, ref[0], ref[1], [], exclude_account); + state = deleteStatus(state, ref, [], exclude_account); }); return state; @@ -122,8 +122,8 @@ const filterTimelines = (state, relationship, statuses) => { return; } - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id); + references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); + state = deleteStatus(state, status.get('id'), references, relationship.id); }); return state; @@ -155,7 +155,7 @@ export default function timelines(state = initialState, action) { case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems, action.filtered); case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); + return deleteStatus(state, action.id, action.references, action.reblogOf); case TIMELINE_CLEAR: return clearTimeline(state, action.timeline); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/flavours/glitch/styles/about.scss b/app/javascript/flavours/glitch/styles/about.scss index a38ca99b4e4..f0a44aa9448 100644 --- a/app/javascript/flavours/glitch/styles/about.scss +++ b/app/javascript/flavours/glitch/styles/about.scss @@ -760,8 +760,13 @@ $small-breakpoint: 960px; } } + &__counters__wrapper { + display: flex; + } + &__counter { padding: 10px; + width: 50%; strong { font-family: $font-display, sans-serif; diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 0d24da4ddf8..1c8f2271f46 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -567,6 +567,18 @@ body, } } +.special-action-button, +.back-link { + text-align: right; + flex: 1 1 auto; +} + +.action-buttons { + display: flex; + overflow: hidden; + justify-content: space-between; +} + .spacer { flex: 1 1 auto; } @@ -904,3 +916,11 @@ a.name-tag, } } } + +.account-badges { + margin: -2px 0; +} + +.dashboard__counters.admin-account-counters { + margin-top: 10px; +} diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index 77631097a71..9ff3f3bacbf 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -150,3 +150,19 @@ button { height: 100%; } } + +.logo-resources { + display: none; +} + +// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements, +// to set the z-index to a high value, which messes with modals and dropdowns. +// Blocked elements can in theory only be media and frames/embeds, so they +// should only appear in statuses, under divs and articles. +body, +div, +article { + .__ns__pop2top { + z-index: unset !important; + } +} diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index 491ceb6ecc2..ccd620215dd 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -80,6 +80,12 @@ &-base { @include avatar-radius(); @include avatar-size(36px); + + img { + @include avatar-radius; + width: 100%; + height: 100%; + } } &-overlay { @@ -90,6 +96,12 @@ bottom: 0; right: 0; z-index: 1; + + img { + @include avatar-radius; + width: 100%; + height: 100%; + } } } diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 396e87c6c12..5de650f0a82 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -133,6 +133,10 @@ code { } } + .otp-hint { + margin-bottom: 25px; + } + .card { margin-bottom: 15px; } @@ -276,6 +280,14 @@ code { margin-bottom: 25px; } } + + .fields-group.invited-by { + margin-bottom: 30px; + + .hint { + text-align: center; + } + } } .input.radio_buttons .radio label { @@ -626,6 +638,15 @@ code { @media screen and (max-width: 740px) and (min-width: 441px) { margin-top: 40px; } + + &.translation-prompt { + text-align: unset; + color: unset; + + a { + text-decoration: underline; + } + } } .form-footer { diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 44338338fd6..5fc41ed9e96 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -25,6 +25,36 @@ } } + progress { + border: 0; + display: block; + width: 100%; + height: 5px; + appearance: none; + background: transparent; + + &::-webkit-progress-bar { + background: transparent; + } + + // Those rules need to be entirely separate or they won't work, hence the + // duplication + &::-moz-progress-bar { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + + &::-ms-fill { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + + &::-webkit-progress-value { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + } + &__option { position: relative; display: flex; diff --git a/app/javascript/flavours/glitch/styles/statuses.scss b/app/javascript/flavours/glitch/styles/statuses.scss index 22fa7b3fd27..4122e121ac7 100644 --- a/app/javascript/flavours/glitch/styles/statuses.scss +++ b/app/javascript/flavours/glitch/styles/statuses.scss @@ -124,6 +124,16 @@ .embed, .public-layout { + .status__content[data-spoiler=folded] { + .e-content { + display: none; + } + + p:first-child { + margin-bottom: 0; + } + } + .detailed-status { padding: 15px; } @@ -162,6 +172,13 @@ .video-player { margin-top: 10px; } + + &__action-bar-button { + font-size: 18px; + width: 23.1429px; + height: 23.1429px; + line-height: 23.15px; + } } } diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 50840cacc24..861827d3385 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -42,7 +42,7 @@ export function updateTimeline(timeline, status, accept) { export function deleteFromTimelines(id) { return (dispatch, getState) => { const accountId = getState().getIn(['statuses', id, 'account']); - const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => [status.get('id'), status.get('account')]); + const references = getState().get('statuses').filter(status => status.get('reblog') === id).map(status => status.get('id')); const reblogOf = getState().getIn(['statuses', id, 'reblog'], null); dispatch({ diff --git a/app/javascript/mastodon/components/dropdown_menu.js b/app/javascript/mastodon/components/dropdown_menu.js index 31c02d735c8..4734e0f3fc4 100644 --- a/app/javascript/mastodon/components/dropdown_menu.js +++ b/app/javascript/mastodon/components/dropdown_menu.js @@ -46,7 +46,7 @@ class DropdownMenu extends React.PureComponent { document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); if (this.focusedItem && this.props.openedViaKeyboard) { - this.focusedItem.focus(); + this.focusedItem.focus({ preventScroll: true }); } this.setState({ mounted: true }); } diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 57588fe960d..96028e0421f 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -100,7 +100,7 @@ class PrivacyDropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus(); + if (this.focusedItem) this.focusedItem.focus({ preventScroll: true }); this.setState({ mounted: true }); } diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 2554c008d10..53dec95859d 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -25,7 +25,7 @@ const importStatuses = (state, statuses) => const deleteStatus = (state, id, references) => { references.forEach(ref => { - state = deleteStatus(state, ref[0], []); + state = deleteStatus(state, ref, []); }); return state.delete(id); diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index 63b76773de9..9156db021d3 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -89,7 +89,7 @@ const updateTimeline = (state, timeline, status, usePendingItems) => { })); }; -const deleteStatus = (state, id, accountId, references, exclude_account = null) => { +const deleteStatus = (state, id, references, exclude_account = null) => { state.keySeq().forEach(timeline => { if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`))) { const helper = list => list.filterNot(item => item === id); @@ -99,7 +99,7 @@ const deleteStatus = (state, id, accountId, references, exclude_account = null) // Remove reblogs of deleted status references.forEach(ref => { - state = deleteStatus(state, ref[0], ref[1], [], exclude_account); + state = deleteStatus(state, ref, [], exclude_account); }); return state; @@ -117,8 +117,8 @@ const filterTimelines = (state, relationship, statuses) => { return; } - references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id); + references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => item.get('id')); + state = deleteStatus(state, status.get('id'), references, relationship.id); }); return state; @@ -150,7 +150,7 @@ export default function timelines(state = initialState, action) { case TIMELINE_UPDATE: return updateTimeline(state, action.timeline, fromJS(action.status), action.usePendingItems); case TIMELINE_DELETE: - return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); + return deleteStatus(state, action.id, action.references, action.reblogOf); case TIMELINE_CLEAR: return clearTimeline(state, action.timeline); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 5b699e76766..3d190d2dace 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -103,15 +103,13 @@ function main() { delegate(document, '.custom-emoji', 'mouseout', getEmojiAnimationHandler('data-static')); delegate(document, '.status__content__spoiler-link', 'click', function() { - const contentEl = this.parentNode.parentNode.querySelector('.e-content'); + const statusEl = this.parentNode.parentNode; - if (contentEl.style.display === 'block') { - contentEl.style.display = 'none'; - this.parentNode.style.marginBottom = 0; + if (statusEl.dataset.spoiler === 'expanded') { + statusEl.dataset.spoiler = 'folded'; this.textContent = (new IntlMessageFormat(messages['status.show_more'] || 'Show more', locale)).format(); } else { - contentEl.style.display = 'block'; - this.parentNode.style.marginBottom = null; + statusEl.dataset.spoiler = 'expanded'; this.textContent = (new IntlMessageFormat(messages['status.show_less'] || 'Show less', locale)).format(); } @@ -119,8 +117,8 @@ function main() { }); [].forEach.call(document.querySelectorAll('.status__content__spoiler-link'), (spoilerLink) => { - const contentEl = spoilerLink.parentNode.parentNode.querySelector('.e-content'); - const message = (contentEl.style.display === 'block') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); + const statusEl = spoilerLink.parentNode.parentNode; + const message = (statusEl.dataset.spoiler === 'expanded') ? (messages['status.show_less'] || 'Show less') : (messages['status.show_more'] || 'Show more'); spoilerLink.textContent = (new IntlMessageFormat(message, locale)).format(); }); }); diff --git a/app/javascript/styles/mastodon/about.scss b/app/javascript/styles/mastodon/about.scss index cf16b54ac17..711f349651c 100644 --- a/app/javascript/styles/mastodon/about.scss +++ b/app/javascript/styles/mastodon/about.scss @@ -757,8 +757,13 @@ $small-breakpoint: 960px; } } + &__counters__wrapper { + display: flex; + } + &__counter { padding: 10px; + width: 50%; strong { font-family: $font-display, sans-serif; diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index 7bff2daa13a..78dea92b972 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -583,6 +583,18 @@ body, } } +.special-action-button, +.back-link { + text-align: right; + flex: 1 1 auto; +} + +.action-buttons { + display: flex; + overflow: hidden; + justify-content: space-between; +} + .spacer { flex: 1 1 auto; } @@ -920,3 +932,11 @@ a.name-tag, } } } + +.account-badges { + margin: -2px 0; +} + +.dashboard__counters.admin-account-counters { + margin-top: 10px; +} diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index 2b10b5ad3c3..a5dbe75fb37 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -229,3 +229,19 @@ button { } } } + +.logo-resources { + display: none; +} + +// NoScript adds a __ns__pop2top class to the full ancestry of blocked elements, +// to set the z-index to a high value, which messes with modals and dropdowns. +// Blocked elements can in theory only be media and frames/embeds, so they +// should only appear in statuses, under divs and articles. +body, +div, +article { + .__ns__pop2top { + z-index: unset !important; + } +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index c7835b87802..6c33b709ddf 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -1362,6 +1362,12 @@ a .account__avatar { &-base { @include avatar-radius; @include avatar-size(36px); + + img { + @include avatar-radius; + width: 100%; + height: 100%; + } } &-overlay { @@ -1372,6 +1378,12 @@ a .account__avatar { bottom: 0; right: 0; z-index: 1; + + img { + @include avatar-radius; + width: 100%; + height: 100%; + } } } diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index c9ad68f9418..0e5b00e8f12 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -142,6 +142,10 @@ code { } } + .otp-hint { + margin-bottom: 25px; + } + .card { margin-bottom: 15px; } @@ -285,6 +289,14 @@ code { margin-bottom: 25px; } } + + .fields-group.invited-by { + margin-bottom: 30px; + + .hint { + text-align: center; + } + } } .input.radio_buttons .radio label { @@ -635,6 +647,15 @@ code { @media screen and (max-width: 740px) and (min-width: 441px) { margin-top: 40px; } + + &.translation-prompt { + text-align: unset; + color: unset; + + a { + text-decoration: underline; + } + } } .form-footer { diff --git a/app/javascript/styles/mastodon/polls.scss b/app/javascript/styles/mastodon/polls.scss index 1ecc8434d6e..ad70889828b 100644 --- a/app/javascript/styles/mastodon/polls.scss +++ b/app/javascript/styles/mastodon/polls.scss @@ -19,6 +19,36 @@ } } + progress { + border: 0; + display: block; + width: 100%; + height: 5px; + appearance: none; + background: transparent; + + &::-webkit-progress-bar { + background: transparent; + } + + // Those rules need to be entirely separate or they won't work, hence the + // duplication + &::-moz-progress-bar { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + + &::-ms-fill { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + + &::-webkit-progress-value { + border-radius: 4px; + background: darken($ui-primary-color, 5%); + } + } + &__option { position: relative; display: flex; diff --git a/app/javascript/styles/mastodon/statuses.scss b/app/javascript/styles/mastodon/statuses.scss index 19ce0ab8f06..0b7be7afdb7 100644 --- a/app/javascript/styles/mastodon/statuses.scss +++ b/app/javascript/styles/mastodon/statuses.scss @@ -128,6 +128,16 @@ .embed, .public-layout { + .status__content[data-spoiler=folded] { + .e-content { + display: none; + } + + p:first-child { + margin-bottom: 0; + } + } + .detailed-status { padding: 15px; } @@ -159,5 +169,12 @@ .video-player { margin-top: 10px; } + + &__action-bar-button { + font-size: 18px; + width: 23.1429px; + height: 23.1429px; + line-height: 23.15px; + } } } diff --git a/app/models/account.rb b/app/models/account.rb index e56db31267f..5038d4768ff 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -3,50 +3,52 @@ # # Table name: accounts # -# id :bigint(8) not null, primary key -# username :string default(""), not null -# domain :string -# secret :string default(""), not null -# private_key :text -# public_key :text default(""), not null -# remote_url :string default(""), not null -# salmon_url :string default(""), not null -# hub_url :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# note :text default(""), not null -# display_name :string default(""), not null -# uri :string default(""), not null -# url :string -# avatar_file_name :string -# avatar_content_type :string -# avatar_file_size :integer -# avatar_updated_at :datetime -# header_file_name :string -# header_content_type :string -# header_file_size :integer -# header_updated_at :datetime -# avatar_remote_url :string -# subscription_expires_at :datetime -# locked :boolean default(FALSE), not null -# header_remote_url :string default(""), not null -# last_webfingered_at :datetime -# inbox_url :string default(""), not null -# outbox_url :string default(""), not null -# shared_inbox_url :string default(""), not null -# followers_url :string default(""), not null -# protocol :integer default("ostatus"), not null -# memorial :boolean default(FALSE), not null -# moved_to_account_id :bigint(8) -# featured_collection_url :string -# fields :jsonb -# actor_type :string -# discoverable :boolean -# also_known_as :string is an Array -# silenced_at :datetime -# suspended_at :datetime -# trust_level :integer -# hide_collections :boolean +# id :bigint(8) not null, primary key +# username :string default(""), not null +# domain :string +# secret :string default(""), not null +# private_key :text +# public_key :text default(""), not null +# remote_url :string default(""), not null +# salmon_url :string default(""), not null +# hub_url :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# note :text default(""), not null +# display_name :string default(""), not null +# uri :string default(""), not null +# url :string +# avatar_file_name :string +# avatar_content_type :string +# avatar_file_size :integer +# avatar_updated_at :datetime +# header_file_name :string +# header_content_type :string +# header_file_size :integer +# header_updated_at :datetime +# avatar_remote_url :string +# subscription_expires_at :datetime +# locked :boolean default(FALSE), not null +# header_remote_url :string default(""), not null +# last_webfingered_at :datetime +# inbox_url :string default(""), not null +# outbox_url :string default(""), not null +# shared_inbox_url :string default(""), not null +# followers_url :string default(""), not null +# protocol :integer default("ostatus"), not null +# memorial :boolean default(FALSE), not null +# moved_to_account_id :bigint(8) +# featured_collection_url :string +# fields :jsonb +# actor_type :string +# discoverable :boolean +# also_known_as :string is an Array +# silenced_at :datetime +# suspended_at :datetime +# trust_level :integer +# hide_collections :boolean +# avatar_storage_schema_version :integer +# header_storage_schema_version :integer # class Account < ApplicationRecord diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb index 96078422270..736da6c1ded 100644 --- a/app/models/concerns/omniauthable.rb +++ b/app/models/concerns/omniauthable.rb @@ -82,7 +82,7 @@ module Omniauthable username = starting_username i = 0 - while Account.exists?(username: username) + while Account.exists?(username: username, domain: nil) i += 1 username = "#{starting_username}_#{i}" end diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index d177cf2815d..7cb03b8199b 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -3,20 +3,21 @@ # # Table name: custom_emojis # -# id :bigint(8) not null, primary key -# shortcode :string default(""), not null -# domain :string -# image_file_name :string -# image_content_type :string -# image_file_size :integer -# image_updated_at :datetime -# created_at :datetime not null -# updated_at :datetime not null -# disabled :boolean default(FALSE), not null -# uri :string -# image_remote_url :string -# visible_in_picker :boolean default(TRUE), not null -# category_id :bigint(8) +# id :bigint(8) not null, primary key +# shortcode :string default(""), not null +# domain :string +# image_file_name :string +# image_content_type :string +# image_file_size :integer +# image_updated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# disabled :boolean default(FALSE), not null +# uri :string +# image_remote_url :string +# visible_in_picker :boolean default(TRUE), not null +# category_id :bigint(8) +# image_storage_schema_version :integer # class CustomEmoji < ApplicationRecord diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 40624c73c0f..f789bdc5531 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -3,23 +3,24 @@ # # Table name: media_attachments # -# id :bigint(8) not null, primary key -# status_id :bigint(8) -# file_file_name :string -# file_content_type :string -# file_file_size :integer -# file_updated_at :datetime -# remote_url :string default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# shortcode :string -# type :integer default("image"), not null -# file_meta :json -# account_id :bigint(8) -# description :text -# scheduled_status_id :bigint(8) -# blurhash :string -# processing :integer +# id :bigint(8) not null, primary key +# status_id :bigint(8) +# file_file_name :string +# file_content_type :string +# file_file_size :integer +# file_updated_at :datetime +# remote_url :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# shortcode :string +# type :integer default("image"), not null +# file_meta :json +# account_id :bigint(8) +# description :text +# scheduled_status_id :bigint(8) +# blurhash :string +# processing :integer +# file_storage_schema_version :integer # class MediaAttachment < ApplicationRecord diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 4e89fbf8510..2802f4667b2 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -3,25 +3,26 @@ # # Table name: preview_cards # -# id :bigint(8) not null, primary key -# url :string default(""), not null -# title :string default(""), not null -# description :string default(""), not null -# image_file_name :string -# image_content_type :string -# image_file_size :integer -# image_updated_at :datetime -# type :integer default("link"), not null -# html :text default(""), not null -# author_name :string default(""), not null -# author_url :string default(""), not null -# provider_name :string default(""), not null -# provider_url :string default(""), not null -# width :integer default(0), not null -# height :integer default(0), not null -# created_at :datetime not null -# updated_at :datetime not null -# embed_url :string default(""), not null +# id :bigint(8) not null, primary key +# url :string default(""), not null +# title :string default(""), not null +# description :string default(""), not null +# image_file_name :string +# image_content_type :string +# image_file_size :integer +# image_updated_at :datetime +# type :integer default("link"), not null +# html :text default(""), not null +# author_name :string default(""), not null +# author_url :string default(""), not null +# provider_name :string default(""), not null +# provider_url :string default(""), not null +# width :integer default(0), not null +# height :integer default(0), not null +# created_at :datetime not null +# updated_at :datetime not null +# embed_url :string default(""), not null +# image_storage_schema_version :integer # class PreviewCard < ApplicationRecord @@ -47,6 +48,10 @@ class PreviewCard < ApplicationRecord before_save :extract_dimensions, if: :link? + def local? + false + end + def missing_image? width.present? && height.present? && image_file_name.blank? end diff --git a/app/models/status.rb b/app/models/status.rb index 31e77770d48..34fa0091258 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -206,12 +206,8 @@ class Status < ApplicationRecord def title if destroyed? "#{account.acct} deleted status" - elsif reblog? - preview = sensitive ? '' : text.slice(0, 10).split("\n")[0] - "#{account.acct} shared #{reblog.account.acct}'s: #{preview}" else - preview = sensitive ? '' : text.slice(0, 20).split("\n")[0] - "#{account.acct}: #{preview}" + reblog? ? "#{account.acct} shared a status by #{reblog.account.acct}" : "New status by #{account.acct}" end end @@ -404,7 +400,7 @@ class Status < ApplicationRecord if account.nil? where(visibility: visibility).not_local_only - elsif target_account.blocking?(account) # get rid of blocked peeps + elsif target_account.blocking?(account) || (account.domain.present? && target_account.domain_blocking?(account.domain)) # get rid of blocked peeps none elsif account.id == target_account.id # author can see own stuff all diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index e913f0c64fd..54e7c450caa 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -5,7 +5,7 @@ class REST::InstanceSerializer < ActiveModel::Serializer attributes :uri, :title, :short_description, :description, :email, :version, :urls, :stats, :thumbnail, :max_toot_chars, :poll_limits, - :languages, :registrations, :approval_required + :languages, :registrations, :approval_required, :invites_enabled has_one :contact_account, serializer: REST::AccountSerializer @@ -76,6 +76,10 @@ class REST::InstanceSerializer < ActiveModel::Serializer Setting.registrations_mode == 'approved' end + def invites_enabled + Setting.min_invite_role == 'user' + end + private def instance_presenter diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 880cdde9224..6c0093cd45e 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -25,7 +25,18 @@ class FetchResourceService < BaseService end def perform_request(&block) - Request.new(:get, @url).add_headers('Accept' => ACCEPT_HEADER).on_behalf_of(Account.representative).perform(&block) + Request.new(:get, @url).tap do |request| + request.add_headers('Accept' => ACCEPT_HEADER) + + # In a real setting we want to sign all outgoing requests, + # in case the remote server has secure mode enabled and requires + # authentication on all resources. However, during development, + # sending request signatures with an inaccessible host is useless + # and prevents even public resources from being fetched, so + # don't do it + + request.on_behalf_of(Account.representative) unless Rails.env.development? + end.perform(&block) end def process_response(response, terminal = false) diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml index e0ec98ec975..07e06100aa9 100644 --- a/app/views/about/show.html.haml +++ b/app/views/about/show.html.haml @@ -68,11 +68,11 @@ .hero-widget__footer__column %h4= t 'about.server_stats' - %div{ style: 'display: flex' } - .hero-widget__counter{ style: 'width: 50%' } + .hero-widget__counters__wrapper + .hero-widget__counter %strong= number_to_human @instance_presenter.user_count, strip_insignificant_zeros: true %span= t 'about.user_count_after', count: @instance_presenter.user_count - .hero-widget__counter{ style: 'width: 50%' } + .hero-widget__counter %strong= number_to_human @instance_presenter.active_user_count, strip_insignificant_zeros: true %span = t 'about.active_count_after' diff --git a/app/views/accounts/_moved.html.haml b/app/views/accounts/_moved.html.haml index a82f277b16c..4f71b062d87 100644 --- a/app/views/accounts/_moved.html.haml +++ b/app/views/accounts/_moved.html.haml @@ -9,8 +9,10 @@ = link_to ActivityPub::TagManager.instance.url_for(moved_to_account), class: 'detailed-status__display-name p-author h-card', target: '_blank', rel: 'me noopener noreferrer' do .detailed-status__display-avatar .account__avatar-overlay - .account__avatar-overlay-base{ style: "background-image: url('#{moved_to_account.avatar.url(:original)}')" } - .account__avatar-overlay-overlay{ style: "background-image: url('#{account.avatar.url(:original)}')" } + .account__avatar-overlay-base + = image_tag moved_to_account.avatar_static_url + .account__avatar-overlay-overlay + = image_tag account.avatar_static_url %span.display-name %bdi diff --git a/app/views/admin/accounts/_account.html.haml b/app/views/admin/accounts/_account.html.haml index 44b10af6e2e..c9bd8c686c2 100644 --- a/app/views/admin/accounts/_account.html.haml +++ b/app/views/admin/accounts/_account.html.haml @@ -2,7 +2,7 @@ %td = admin_account_link_to(account) %td - %div{ style: 'margin: -2px 0' }= account_badge(account, all: true) + %div.account-badges= account_badge(account, all: true) %td - if account.user_current_sign_in_ip %samp.ellipsized-ip{ title: account.user_current_sign_in_ip }= account.user_current_sign_in_ip diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml index 408f94eed8b..e6461aad04d 100644 --- a/app/views/admin/accounts/show.html.haml +++ b/app/views/admin/accounts/show.html.haml @@ -31,7 +31,7 @@ %div .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true) -.dashboard__counters{ style: 'margin-top: 10px' } +.dashboard__counters.admin-account-counters %div = link_to admin_account_statuses_path(@account.id) do .dashboard__counters__num= number_with_delimiter @account.statuses_count @@ -178,18 +178,8 @@ = @account.shared_inbox_url = fa_icon DeliveryFailureTracker.available?(@account.shared_inbox_url) ? 'check': 'times' - %div{ style: 'overflow: hidden' } - %div{ style: 'float: right' } - - if @account.local? - = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) - - if @account.user&.otp_required_for_login? - = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) - - if !@account.memorial? && @account.user_approved? - = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) - - else - = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) - - %div{ style: 'float: left' } + %div.action-buttons + %div - if @account.local? && @account.user_approved? = link_to t('admin.accounts.warn'), new_admin_account_action_path(@account.id, type: 'none'), class: 'button' if can?(:warn, @account) - if @account.silenced? @@ -216,6 +206,16 @@ - else = link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @account.domain), class: 'button button--destructive' + %div + - if @account.local? + = link_to t('admin.accounts.reset_password'), admin_account_reset_path(@account.id), method: :create, class: 'button' if can?(:reset_password, @account.user) + - if @account.user&.otp_required_for_login? + = link_to t('admin.accounts.disable_two_factor_authentication'), admin_user_two_factor_authentication_path(@account.user.id), method: :delete, class: 'button' if can?(:disable_2fa, @account.user) + - if !@account.memorial? && @account.user_approved? + = link_to t('admin.accounts.memorialize'), memorialize_admin_account_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:memorialize, @account) + - else + = link_to t('admin.accounts.redownload'), redownload_admin_account_path(@account.id), method: :post, class: 'button' if can?(:redownload, @account) + %hr.spacer/ - unless @warnings.empty? diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 0b299acc528..bd67eb4fc63 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -10,7 +10,7 @@ - unless whitelist_mode? %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1' - %div{ style: 'flex: 1 1 auto; text-align: right' } + %div.special-action-button - if whitelist_mode? = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button' - else diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index 49a666a5a2d..92e14c0df06 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -45,11 +45,11 @@ %hr.spacer/ -%div{ style: 'overflow: hidden' } - %div{ style: 'float: left' } +%div.action-buttons + %div = link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button' - %div{ style: 'float: right' } + %div - if @domain_allow = link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete } - elsif @domain_block diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml index 171976e333e..8101d7f99a8 100644 --- a/app/views/admin/pending_accounts/index.html.haml +++ b/app/views/admin/pending_accounts/index.html.haml @@ -22,9 +22,9 @@ %hr.spacer/ -%div{ style: 'overflow: hidden' } - %div{ style: 'float: right' } - = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' - +%div.action-buttons %div = link_to t('admin.accounts.approve_all'), approve_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + + %div + = link_to t('admin.accounts.reject_all'), reject_all_admin_pending_accounts_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' diff --git a/app/views/admin/relationships/index.html.haml b/app/views/admin/relationships/index.html.haml index 3afaff61538..907477f246c 100644 --- a/app/views/admin/relationships/index.html.haml +++ b/app/views/admin/relationships/index.html.haml @@ -17,7 +17,7 @@ %li= filter_link_to t('admin.accounts.location.local'), location: 'local' %li= filter_link_to t('admin.accounts.location.remote'), location: 'remote' - .back-link{ style: 'flex: 1 1 auto; text-align: right' } + .back-link = link_to admin_account_path(@account.id) do = fa_icon 'chevron-left fw' = t('admin.statuses.back_to_account') diff --git a/app/views/admin/reports/show.html.haml b/app/views/admin/reports/show.html.haml index b12ea4270e5..4ecc8dc9395 100644 --- a/app/views/admin/reports/show.html.haml +++ b/app/views/admin/reports/show.html.haml @@ -65,9 +65,11 @@ %hr.spacer -%div{ style: 'overflow: hidden; margin-bottom: 20px; clear: both' } +%div.action-buttons + %div + - if @report.unresolved? - %div{ style: 'float: right' } + %div - if @report.target_account.local? = link_to t('admin.accounts.warn'), new_admin_account_action_path(@report.target_account_id, type: 'none', report_id: @report.id), class: 'button' = link_to t('admin.accounts.disable'), new_admin_account_action_path(@report.target_account_id, type: 'disable', report_id: @report.id), class: 'button button--destructive' diff --git a/app/views/admin/statuses/index.html.haml b/app/views/admin/statuses/index.html.haml index 55926f3b32e..5414d69d504 100644 --- a/app/views/admin/statuses/index.html.haml +++ b/app/views/admin/statuses/index.html.haml @@ -9,7 +9,7 @@ %ul %li= link_to t('admin.statuses.no_media'), admin_account_statuses_path(@account.id, current_params.merge(media: nil)), class: !params[:media] && 'selected' %li= link_to t('admin.statuses.with_media'), admin_account_statuses_path(@account.id, current_params.merge(media: true)), class: params[:media] && 'selected' - .back-link{ style: 'flex: 1 1 auto; text-align: right' } + .back-link = link_to admin_account_path(@account.id) do = fa_icon 'chevron-left fw' = t('admin.statuses.back_to_account') diff --git a/app/views/admin/statuses/show.html.haml b/app/views/admin/statuses/show.html.haml index a7a392272fc..e2470198d66 100644 --- a/app/views/admin/statuses/show.html.haml +++ b/app/views/admin/statuses/show.html.haml @@ -4,7 +4,7 @@ = "@#{@account.acct}" .filters - .back-link{ style: 'flex: 1 1 auto; text-align: right' } + .back-link = link_to admin_account_path(@account.id) do %i.fa.fa-chevron-left.fa-fw = t('admin.statuses.back_to_account') diff --git a/app/views/admin/tags/index.html.haml b/app/views/admin/tags/index.html.haml index d20ed80f889..e6480227544 100644 --- a/app/views/admin/tags/index.html.haml +++ b/app/views/admin/tags/index.html.haml @@ -68,9 +68,9 @@ - if params[:pending_review] == '1' || params[:unreviewed] == '1' %hr.spacer/ - %div{ style: 'overflow: hidden' } - %div{ style: 'float: right' } - = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' - + %div.action-buttons %div = link_to t('admin.accounts.approve_all'), approve_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button' + + %div + = link_to t('admin.accounts.reject_all'), reject_all_admin_tags_path, method: :post, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml index 808dce5144b..e7ecfecd9a1 100644 --- a/app/views/application/_card.html.haml +++ b/app/views/application/_card.html.haml @@ -9,7 +9,6 @@ = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' .display-name - %span{ id: "default_account_display_name", style: "display: none" }= account.username %bdi %strong.emojify.p-name= display_name(account, custom_emojify: true) %span diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml index bcd66fb8a00..457bc1d2314 100644 --- a/app/views/auth/registrations/new.html.haml +++ b/app/views/auth/registrations/new.html.haml @@ -8,8 +8,8 @@ = render 'shared/error_messages', object: resource - if @invite.present? && @invite.autofollow? - .fields-group{ style: 'margin-bottom: 30px' } - %p.hint{ style: 'text-align: center' }= t('invites.invited_by') + .fields-group.invited-by + %p.hint= t('invites.invited_by') = render 'application/card', account: @invite.user.account = f.simple_fields_for :account do |ff| diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml index 4e6bbd7a958..b2e36f6bca4 100644 --- a/app/views/auth/sessions/two_factor.html.haml +++ b/app/views/auth/sessions/two_factor.html.haml @@ -2,7 +2,7 @@ = t('auth.login') = simple_form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f| - %p.hint{ style: 'margin-bottom: 25px' }= t('simple_form.hints.sessions.otp') + %p.hint.otp-hint= t('simple_form.hints.sessions.otp') .fields-group = f.input :otp_attempt, type: :number, wrapper: :with_label, label: t('simple_form.labels.defaults.otp_attempt'), input_html: { 'aria-label' => t('simple_form.labels.defaults.otp_attempt'), :autocomplete => 'off' }, autofocus: true diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml index ecf12b6490d..1170332ffb0 100644 --- a/app/views/directories/index.html.haml +++ b/app/views/directories/index.html.haml @@ -28,7 +28,6 @@ = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo' .display-name - %span{ id: "default_account_display_name", style: "display: none" }= account.username %bdi %strong.emojify.p-name= display_name(account, custom_emojify: true) %span= acct(account) diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 2be9427c568..99ab3729e0f 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -40,6 +40,6 @@ %body{ class: body_classes } = content_for?(:content) ? yield(:content) : yield - %div{ style: 'display: none'} + .logo-resources = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg') = render file: Rails.root.join('app', 'javascript', 'images', 'logo_full.svg') diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 6695b12dd6e..75441b452fe 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -23,5 +23,5 @@ %body.embed = yield - %div{ style: 'display: none'} + .logo-resources = render file: Rails.root.join('app', 'javascript', 'images', 'logo_transparent.svg') diff --git a/app/views/public_timelines/show.html.haml b/app/views/public_timelines/show.html.haml index 063089a7faf..e32bd49ec4b 100644 --- a/app/views/public_timelines/show.html.haml +++ b/app/views/public_timelines/show.html.haml @@ -12,5 +12,5 @@ - else %p= t('about.browse_local_posts') -#mastodon-timeline{ data: { props: Oj.dump(default_props) }} +#mastodon-timeline{ data: { props: Oj.dump(default_props.merge(local: !Setting.show_known_fediverse_at_about_page)) }} #modal-container diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 5453177fdbe..5fc865814a0 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -9,8 +9,8 @@ = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false - unless I18n.locale == :en - .flash-message{ style: "text-align: unset; color: unset" } - #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")} + .flash-message.translation-prompt + #{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener")} %h4= t 'appearance.advanced_web_interface' diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml index 841c01fd7b6..6061e9cfdf8 100644 --- a/app/views/settings/profiles/show.html.haml +++ b/app/views/settings/profiles/show.html.haml @@ -9,7 +9,7 @@ .fields-row .fields-row__column.fields-group.fields-row__column-6 - = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH }, hint: false + = f.input :display_name, wrapper: :with_label, input_html: { maxlength: Account::MAX_DISPLAY_NAME_LENGTH, data: { default: @account.username } }, hint: false = f.input :note, wrapper: :with_label, input_html: { maxlength: Account::MAX_NOTE_LENGTH }, hint: false .fields-row diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml index 021390e474f..544b9233080 100644 --- a/app/views/statuses/_detailed_status.html.haml +++ b/app/views/statuses/_detailed_status.html.haml @@ -15,12 +15,12 @@ = account_action_button(status.account) - .status__content.emojify< + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %p< %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" } + .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' } = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.preloadable_poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do diff --git a/app/views/statuses/_poll.html.haml b/app/views/statuses/_poll.html.haml index de5357e6d8c..64e62e97c01 100644 --- a/app/views/statuses/_poll.html.haml +++ b/app/views/statuses/_poll.html.haml @@ -10,13 +10,15 @@ - percent = total_votes_count > 0 ? 100 * option.votes_count / total_votes_count : 0 %label.poll__option>< %span.poll__number>< - - if own_votes.include?(index) - %i.poll__voted__mark.fa.fa-check = "#{percent.round}%" %span.poll__option__text = Formatter.instance.format_poll_option(status, option, autoplay: autoplay) + - if own_votes.include?(index) + %span.poll__voted + %i.poll__voted__mark.fa.fa-check - %span.poll__chart{ style: "width: #{percent}%" } + %progress{ max: 100, value: percent < 1 ? 1 : percent, 'aria-hidden': 'true' } + %span.poll__chart - else %label.poll__option>< %span.poll__input{ class: poll.multiple? ? 'checkbox' : nil}>< diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 8a418a1d5cb..f959056cdc6 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -19,12 +19,12 @@ %span.display-name__account = acct(status.account) = fa_icon('lock') if status.account.locked? - .status__content.emojify< + .status__content.emojify{ :data => ({ spoiler: current_account&.user&.setting_expand_spoilers ? 'expanded' : 'folded' } if status.spoiler_text?) }< - if status.spoiler_text? - %p{ :style => ('margin-bottom: 0' unless current_account&.user&.setting_expand_spoilers) }< + %p< %span.p-summary> #{Formatter.instance.format_spoiler(status, autoplay: autoplay)}  %button.status__content__spoiler-link= t('statuses.show_more') - .e-content{ style: "display: #{!current_account&.user&.setting_expand_spoilers && status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl_status?(status) ? 'rtl' : 'ltr'}" }< + .e-content{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }< = Formatter.instance.format(status, custom_emojify: true, autoplay: autoplay) - if status.preloadable_poll = react_component :poll, disabled: true, poll: ActiveModelSerializers::SerializableResource.new(status.preloadable_poll, serializer: REST::PollSerializer, scope: current_user, scope_name: :current_user).as_json do @@ -51,18 +51,18 @@ .status__action-bar .status__action-bar__counter - = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do - if status.in_reply_to_id.nil? = fa_icon 'reply fw' - else = fa_icon 'reply-all fw' .status__action-bar__counter__label= obscured_counter status.replies_count - = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do - if status.distributable? = fa_icon 'retweet fw' - elsif status.private_visibility? || status.limited_visibility? = fa_icon 'lock fw' - else = fa_icon 'envelope fw' - = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button', style: 'font-size: 18px; width: 23.1429px; height: 23.1429px; line-height: 23.15px;' do + = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do = fa_icon 'star fw' diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 8909678d653..0dd7f8cf8ba 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -10,9 +10,25 @@ Paperclip.interpolates :filename do |attachment, style| end end +Paperclip.interpolates :prefix_path do |attachment, style| + if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local? + 'cache' + File::SEPARATOR + else + '' + end +end + +Paperclip.interpolates :prefix_url do |attachment, style| + if attachment.storage_schema_version >= 1 && attachment.instance.respond_to?(:local?) && !attachment.instance.local? + 'cache/' + else + '' + end +end + Paperclip::Attachment.default_options.merge!( use_timestamp: false, - path: ':class/:attachment/:id_partition/:style/:filename', + path: ':prefix_url:class/:attachment/:id_partition/:style/:filename', storage: :fog ) @@ -91,7 +107,7 @@ else Paperclip::Attachment.default_options.merge!( storage: :filesystem, use_timestamp: true, - path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':class', ':attachment', ':id_partition', ':style', ':filename'), - url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:class/:attachment/:id_partition/:style/:filename', + path: File.join(ENV.fetch('PAPERCLIP_ROOT_PATH', File.join(':rails_root', 'public', 'system')), ':prefix_path:class', ':attachment', ':id_partition', ':style', ':filename'), + url: ENV.fetch('PAPERCLIP_ROOT_URL', '/system') + '/:prefix_url:class/:attachment/:id_partition/:style/:filename', ) end diff --git a/db/migrate/20200417125749_add_storage_schema_version.rb b/db/migrate/20200417125749_add_storage_schema_version.rb new file mode 100644 index 00000000000..7438f97bae8 --- /dev/null +++ b/db/migrate/20200417125749_add_storage_schema_version.rb @@ -0,0 +1,9 @@ +class AddStorageSchemaVersion < ActiveRecord::Migration[5.2] + def change + add_column :preview_cards, :image_storage_schema_version, :integer + add_column :accounts, :avatar_storage_schema_version, :integer + add_column :accounts, :header_storage_schema_version, :integer + add_column :media_attachments, :file_storage_schema_version, :integer + add_column :custom_emojis, :image_storage_schema_version, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 36e9017b595..2b27a187722 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_04_07_202420) do +ActiveRecord::Schema.define(version: 2020_04_17_125749) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -172,6 +172,8 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do t.datetime "suspended_at" t.integer "trust_level" t.boolean "hide_collections" + t.integer "avatar_storage_schema_version" + t.integer "header_storage_schema_version" t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), lower((domain)::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id" @@ -299,6 +301,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do t.string "image_remote_url" t.boolean "visible_in_picker", default: true, null: false t.bigint "category_id" + t.integer "image_storage_schema_version" t.index ["shortcode", "domain"], name: "index_custom_emojis_on_shortcode_and_domain", unique: true end @@ -465,6 +468,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do t.bigint "scheduled_status_id" t.string "blurhash" t.integer "processing" + t.integer "file_storage_schema_version" t.index ["account_id"], name: "index_media_attachments_on_account_id" t.index ["scheduled_status_id"], name: "index_media_attachments_on_scheduled_status_id" t.index ["shortcode"], name: "index_media_attachments_on_shortcode", unique: true @@ -605,6 +609,7 @@ ActiveRecord::Schema.define(version: 2020_04_07_202420) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.string "embed_url", default: "", null: false + t.integer "image_storage_schema_version" t.index ["url"], name: "index_preview_cards_on_url", unique: true end diff --git a/ide-helper.js b/ide-helper.js new file mode 100644 index 00000000000..9e645cb0ebe --- /dev/null +++ b/ide-helper.js @@ -0,0 +1,12 @@ +/* global path */ +/* +Preferences | Languages & Frameworks | JavaScript | Webpack | webpack configuration file +jetbrains://WebStorm/settings?name=Languages+%26+Frameworks--JavaScript--Webpack +*/ +module.exports = { + resolve: { + alias: { + 'mastodon': path.resolve(__dirname, 'app/javascript/mastodon'), + }, + }, +}; diff --git a/lib/cli.rb b/lib/cli.rb index 19cc5d6b519..313a36a3d98 100644 --- a/lib/cli.rb +++ b/lib/cli.rb @@ -11,6 +11,7 @@ require_relative 'mastodon/statuses_cli' require_relative 'mastodon/domains_cli' require_relative 'mastodon/preview_cards_cli' require_relative 'mastodon/cache_cli' +require_relative 'mastodon/upgrade_cli' require_relative 'mastodon/version' module Mastodon @@ -49,6 +50,9 @@ module Mastodon desc 'cache SUBCOMMAND ...ARGS', 'Manage cache' subcommand 'cache', Mastodon::CacheCLI + desc 'upgrade SUBCOMMAND ...ARGS', 'Various version upgrade utilities' + subcommand 'upgrade', Mastodon::UpgradeCLI + option :dry_run, type: :boolean desc 'self-destruct', 'Erase the server from the federation' long_desc <<~LONG_DESC diff --git a/lib/mastodon/cli_helper.rb b/lib/mastodon/cli_helper.rb index ec4d9a81e5f..4a20fa8d64b 100644 --- a/lib/mastodon/cli_helper.rb +++ b/lib/mastodon/cli_helper.rb @@ -10,6 +10,10 @@ Paperclip.options[:log] = false module Mastodon module CLIHelper + def dry_run? + options[:dry_run] + end + def create_progress_bar(total = nil) ProgressBar.create(total: total, format: '%c/%u |%b%i| %e') end diff --git a/lib/mastodon/emoji_cli.rb b/lib/mastodon/emoji_cli.rb index dbaf120182f..da8fd6a0dc2 100644 --- a/lib/mastodon/emoji_cli.rb +++ b/lib/mastodon/emoji_cli.rb @@ -23,7 +23,7 @@ module Mastodon Existing emoji will be skipped unless the --overwrite option is provided, in which case they will be overwritten. - You can specifiy a --category under which the emojis will be + You can specify a --category under which the emojis will be grouped together. With the --prefix option, a prefix can be added to all @@ -72,6 +72,48 @@ module Mastodon say("Imported #{imported}, skipped #{skipped}, failed to import #{failed}", color(imported, skipped, failed)) end + option :category + option :overwrite, type: :boolean + desc 'export PATH', 'Export emoji to a TAR GZIP archive at PATH' + long_desc <<-LONG_DESC + Exports custom emoji to 'export.tar.gz' at PATH. + + The --category option dumps only the specified category. + If this option is not specified, all emoji will be exported. + + The --overwrite option will overwrite an existing archive. + LONG_DESC + def export(path) + exported = 0 + category = CustomEmojiCategory.find_by(name: options[:category]) + export_file_name = File.join(path, 'export.tar.gz') + + if File.file?(export_file_name) && !options[:overwrite] + say("Archive already exists! Use '--overwrite' to overwrite it!") + exit 1 + end + if category.nil? && options[:category] + say("Unable to find category '#{options[:category]}'!") + exit 1 + end + + File.open(export_file_name, 'wb') do |file| + Zlib::GzipWriter.wrap(file) do |gzip| + Gem::Package::TarWriter.new(gzip) do |tar| + scope = !options[:category] || category.nil? ? CustomEmoji.local : category.emojis + scope.find_each do |emoji| + say("Adding '#{emoji.shortcode}'...") + tar.add_file_simple(emoji.shortcode + File.extname(emoji.image_file_name), 0o644, emoji.image_file_size) do |io| + io.write Paperclip.io_adapters.for(emoji.image).read + exported += 1 + end + end + end + end + end + say("Exported #{exported}") + end + option :remote_only, type: :boolean desc 'purge', 'Remove all custom emoji' long_desc <<-LONG_DESC diff --git a/lib/mastodon/media_cli.rb b/lib/mastodon/media_cli.rb index 0f211f2726a..424d65a5f4e 100644 --- a/lib/mastodon/media_cli.rb +++ b/lib/mastodon/media_cli.rb @@ -85,7 +85,9 @@ module Mastodon record_map = preload_records_from_mixed_objects(objects) objects.each do |object| - path_segments = object.key.split('/') + path_segments = object.key.split('/') + path_segments.delete('cache') + model_name = path_segments.first.classify attachment_name = path_segments[1].singularize record_id = path_segments[2..-2].join.to_i @@ -120,8 +122,11 @@ module Mastodon Find.find(File.join(*[root_path, prefix].compact)) do |path| next if File.directory?(path) - key = path.gsub("#{root_path}#{File::SEPARATOR}", '') - path_segments = key.split(File::SEPARATOR) + key = path.gsub("#{root_path}#{File::SEPARATOR}", '') + + path_segments = key.split(File::SEPARATOR) + path_segments.delete('cache') + model_name = path_segments.first.classify record_id = path_segments[2..-2].join.to_i attachment_name = path_segments[1].singularize @@ -229,10 +234,13 @@ module Mastodon desc 'lookup URL', 'Lookup where media is displayed by passing a media URL' def lookup(url) - path = Addressable::URI.parse(url).path + path = Addressable::URI.parse(url).path + path_segments = path.split('/')[2..-1] - model_name = path_segments.first.classify - record_id = path_segments[2..-2].join.to_i + path_segments.delete('cache') + + model_name = path_segments.first.classify + record_id = path_segments[2..-2].join.to_i unless PRELOAD_MODEL_WHITELIST.include?(model_name) say("Cannot find corresponding model: #{model_name}", :red) @@ -276,7 +284,9 @@ module Mastodon preload_map = Hash.new { |hash, key| hash[key] = [] } objects.map do |object| - segments = object.key.split('/') + segments = object.key.split('/') + segments.delete('cache') + model_name = segments.first.classify record_id = segments[2..-2].join.to_i diff --git a/lib/mastodon/upgrade_cli.rb b/lib/mastodon/upgrade_cli.rb new file mode 100644 index 00000000000..74d13f62d9b --- /dev/null +++ b/lib/mastodon/upgrade_cli.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +require_relative '../../config/boot' +require_relative '../../config/environment' +require_relative 'cli_helper' + +module Mastodon + class UpgradeCLI < Thor + include CLIHelper + + def self.exit_on_failure? + true + end + + CURRENT_STORAGE_SCHEMA_VERSION = 1 + + option :dry_run, type: :boolean, default: false + option :verbose, type: :boolean, default: false, aliases: [:v] + desc 'storage-schema', 'Upgrade storage schema of various file attachments to the latest version' + long_desc <<~LONG_DESC + Iterates over every file attachment of every record and, if its storage schema is outdated, performs the + necessary upgrade to the latest one. In practice this means e.g. moving files to different directories. + + Will most likely take a long time. + LONG_DESC + def storage_schema + progress = create_progress_bar(nil) + dry_run = dry_run? ? ' (DRY RUN)' : '' + records = 0 + + klasses = [ + Account, + CustomEmoji, + MediaAttachment, + PreviewCard, + ] + + klasses.each do |klass| + attachment_names = klass.attachment_definitions.keys + + klass.find_each do |record| + attachment_names.each do |attachment_name| + attachment = record.public_send(attachment_name) + + next if attachment.blank? || attachment.storage_schema_version >= CURRENT_STORAGE_SCHEMA_VERSION + + attachment.styles.each_key do |style| + case Paperclip::Attachment.default_options[:storage] + when :s3 + upgrade_storage_s3(progress, attachment, style) + when :fog + upgrade_storage_fog(progress, attachment, style) + when :filesystem + upgrade_storage_filesystem(progress, attachment, style) + end + + progress.increment + end + + attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION) + end + + if record.changed? + record.save unless dry_run? + records += 1 + end + end + end + + progress.total = progress.progress + progress.finish + + say("Upgraded storage schema of #{records} records#{dry_run}", :green, true) + end + + private + + def upgrade_storage_s3(progress, attachment, style) + previous_storage_schema_version = attachment.storage_schema_version + object = attachment.s3_object(style) + + attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION) + + upgraded_path = attachment.path(style) + + if upgraded_path != object.key && object.exists? + progress.log("Moving #{object.key} to #{upgraded_path}") if options[:verbose] + + begin + object.move_to(upgraded_path) unless dry_run? + rescue => e + progress.log(pastel.red("Error processing #{object.key}: #{e}")) + end + end + + # Because we move files style-by-style, it's important to restore + # previous version at the end. The upgrade will be recorded after + # all styles are updated + attachment.instance_write(:storage_schema_version, previous_storage_schema_version) + end + + def upgrade_storage_fog(_progress, _attachment, _style) + say('The fog storage driver is not supported for this operation at this time', :red) + exit(1) + end + + def upgrade_storage_filesystem(progress, attachment, style) + previous_storage_schema_version = attachment.storage_schema_version + previous_path = attachment.path(style) + + attachment.instance_write(:storage_schema_version, CURRENT_STORAGE_SCHEMA_VERSION) + + upgraded_path = attachment.path(style) + + if upgraded_path != previous_path && File.exist?(previous_path) + progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose] + + begin + unless dry_run? + FileUtils.mkdir_p(File.dirname(upgraded_path)) + FileUtils.mv(previous_path, upgraded_path) + + begin + FileUtils.rmdir(previous_path, parents: true) + rescue Errno::ENOTEMPTY + # OK + end + end + rescue => e + progress.log(pastel.red("Error processing #{previous_path}: #{e}")) + + unless dry_run? + begin + FileUtils.rmdir(upgraded_path, parents: true) + rescue Errno::ENOTEMPTY + # OK + end + end + end + end + + # Because we move files style-by-style, it's important to restore + # previous version at the end. The upgrade will be recorded after + # all styles are updated + attachment.instance_write(:storage_schema_version, previous_storage_schema_version) + end + end +end diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index ce578055792..f3e51dbd38c 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -14,6 +14,15 @@ module Paperclip end end + def storage_schema_version + instance_read(:storage_schema_version) || 0 + end + + def assign_attributes + super + instance_write(:storage_schema_version, 1) + end + def variant?(other_filename) return true if original_filename == other_filename return false if original_filename.nil? diff --git a/spec/controllers/activitypub/collections_controller_spec.rb b/spec/controllers/activitypub/collections_controller_spec.rb index 34114cc85c6..56be49be3d7 100644 --- a/spec/controllers/activitypub/collections_controller_spec.rb +++ b/spec/controllers/activitypub/collections_controller_spec.rb @@ -3,21 +3,133 @@ require 'rails_helper' RSpec.describe ActivityPub::CollectionsController, type: :controller do - describe 'POST #show' do - let(:account) { Fabricate(:account) } + let!(:account) { Fabricate(:account) } + let(:remote_account) { nil } - context 'id is "featured"' do - it 'returns 200 with "application/activity+json"' do - post :show, params: { id: 'featured', account_username: account.username } + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) - expect(response).to have_http_status(200) - expect(response.content_type).to eq 'application/activity+json' + Fabricate(:status_pin, account: account) + Fabricate(:status_pin, account: account) + Fabricate(:status, account: account, visibility: :private) + end + + describe 'GET #show' do + context 'when id is "featured"' do + context 'without signature' do + let(:remote_account) { nil } + + before do + get :show, params: { id: 'featured', account_username: account.username } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns orderedItems with pinned statuses' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 2 + end + end + + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + context do + before do + get :show, params: { id: 'featured', account_username: account.username } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns orderedItems with pinned statuses' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 2 + end + end + + context 'in authorized fetch mode' do + before do + allow(controller).to receive(:authorized_fetch_mode?).and_return(true) + end + + context 'when signed request account is blocked' do + before do + account.block!(remote_account) + get :show, params: { id: 'featured', account_username: account.username } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns empty orderedItems' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 0 + end + end + + context 'when signed request account is domain blocked' do + before do + account.block_domain!(remote_account.domain) + get :show, params: { id: 'featured', account_username: account.username } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns empty orderedItems' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 0 + end + end + end end end - context 'id is not "featured"' do - it 'returns 404' do - post :show, params: { id: 'hoge', account_username: account.username } + context 'when id is not "featured"' do + it 'returns http not found' do + get :show, params: { id: 'hoge', account_username: account.username } expect(response).to have_http_status(404) end end diff --git a/spec/controllers/activitypub/inboxes_controller_spec.rb b/spec/controllers/activitypub/inboxes_controller_spec.rb index a9ee7549002..f3bc23953a5 100644 --- a/spec/controllers/activitypub/inboxes_controller_spec.rb +++ b/spec/controllers/activitypub/inboxes_controller_spec.rb @@ -3,25 +3,31 @@ require 'rails_helper' RSpec.describe ActivityPub::InboxesController, type: :controller do - describe 'POST #create' do - context 'with signed_request_account' do - it 'returns 202' do - allow(controller).to receive(:signed_request_account) do - Fabricate(:account) - end + let(:remote_account) { nil } + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) + end + + describe 'POST #create' do + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub) } + + before do post :create, body: '{}' + end + + it 'returns http accepted' do expect(response).to have_http_status(202) end end - context 'without signed_request_account' do - it 'returns 401' do - allow(controller).to receive(:signed_request_account) do - false - end - + context 'without signature' do + before do post :create, body: '{}' + end + + it 'returns http not authorized' do expect(response).to have_http_status(401) end end diff --git a/spec/controllers/activitypub/outboxes_controller_spec.rb b/spec/controllers/activitypub/outboxes_controller_spec.rb index 47460b22ce6..03490533dd5 100644 --- a/spec/controllers/activitypub/outboxes_controller_spec.rb +++ b/spec/controllers/activitypub/outboxes_controller_spec.rb @@ -4,20 +4,174 @@ RSpec.describe ActivityPub::OutboxesController, type: :controller do let!(:account) { Fabricate(:account) } before do - Fabricate(:status, account: account) + Fabricate(:status, account: account, visibility: :public) + Fabricate(:status, account: account, visibility: :unlisted) + Fabricate(:status, account: account, visibility: :private) + Fabricate(:status, account: account, visibility: :direct) + Fabricate(:status, account: account, visibility: :limited) + end + + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) end describe 'GET #show' do - before do - get :show, params: { account_username: account.username } + context 'without signature' do + let(:remote_account) { nil } + + before do + get :show, params: { account_username: account.username, page: page } + end + + context 'with page not requested' do + let(:page) { nil } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns totalItems' do + json = body_as_json + expect(json[:totalItems]).to eq 4 + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + end + + context 'with page requested' do + let(:page) { 'true' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns orderedItems with public or unlisted statuses' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 2 + expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + end end - it 'returns http success' do - expect(response).to have_http_status(200) - end + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:page) { 'true' } - it 'returns application/activity+json' do - expect(response.content_type).to eq 'application/activity+json' + context 'when signed request account does not follow account' do + before do + get :show, params: { account_username: account.username, page: page } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns orderedItems with public or unlisted statuses' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 2 + expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to eq 'max-age=0, private' + end + end + + context 'when signed request account follows account' do + before do + remote_account.follow!(account) + get :show, params: { account_username: account.username, page: page } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns orderedItems with private statuses' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 3 + expect(json[:orderedItems].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:to].include?(account_followers_url(account, ActionMailer::Base.default_url_options)) }).to be true + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to eq 'max-age=0, private' + end + end + + context 'when signed request account is blocked' do + before do + account.block!(remote_account) + get :show, params: { account_username: account.username, page: page } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns empty orderedItems' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 0 + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to eq 'max-age=0, private' + end + end + + context 'when signed request account is domain blocked' do + before do + account.block_domain!(remote_account.domain) + get :show, params: { account_username: account.username, page: page } + end + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns empty orderedItems' do + json = body_as_json + expect(json[:orderedItems]).to be_an Array + expect(json[:orderedItems].size).to eq 0 + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to eq 'max-age=0, private' + end + end end end end diff --git a/spec/controllers/activitypub/replies_controller_spec.rb b/spec/controllers/activitypub/replies_controller_spec.rb new file mode 100644 index 00000000000..a5ed141809c --- /dev/null +++ b/spec/controllers/activitypub/replies_controller_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::RepliesController, type: :controller do + let(:status) { Fabricate(:status, visibility: parent_visibility) } + let(:remote_account) { nil } + + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) + + Fabricate(:status, thread: status, visibility: :public) + Fabricate(:status, thread: status, visibility: :public) + Fabricate(:status, thread: status, visibility: :private) + Fabricate(:status, account: status.account, thread: status, visibility: :public) + Fabricate(:status, account: status.account, thread: status, visibility: :private) + end + + describe 'GET #index' do + context 'with no signature' do + before do + get :index, params: { account_username: status.account.username, status_id: status.id } + end + + context 'when status is public' do + let(:parent_visibility) { :public } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns items with account\'s own replies' do + json = body_as_json + + expect(json[:first]).to be_a Hash + expect(json[:first][:items]).to be_an Array + expect(json[:first][:items].size).to eq 1 + expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true + end + end + + context 'when status is private' do + let(:parent_visibility) { :private } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is direct' do + let(:parent_visibility) { :direct } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + let(:only_other_accounts) { nil } + + context do + before do + get :index, params: { account_username: status.account.username, status_id: status.id, only_other_accounts: only_other_accounts } + end + + context 'when status is public' do + let(:parent_visibility) { :public } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns application/activity+json' do + expect(response.content_type).to eq 'application/activity+json' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + context 'without only_other_accounts' do + it 'returns items with account\'s own replies' do + json = body_as_json + + expect(json[:first]).to be_a Hash + expect(json[:first][:items]).to be_an Array + expect(json[:first][:items].size).to eq 1 + expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true + end + end + + context 'with only_other_accounts' do + let(:only_other_accounts) { 'true' } + + it 'returns items with other public or unlisted replies' do + json = body_as_json + + expect(json[:first]).to be_a Hash + expect(json[:first][:items]).to be_an Array + expect(json[:first][:items].size).to eq 2 + expect(json[:first][:items].all? { |item| item[:to].include?(ActivityPub::TagManager::COLLECTIONS[:public]) || item[:cc].include?(ActivityPub::TagManager::COLLECTIONS[:public]) }).to be true + end + end + end + + context 'when status is private' do + let(:parent_visibility) { :private } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is direct' do + let(:parent_visibility) { :direct } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + + context 'when signed request account is blocked' do + before do + status.account.block!(remote_account) + get :index, params: { account_username: status.account.username, status_id: status.id } + end + + context 'when status is public' do + let(:parent_visibility) { :public } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is private' do + let(:parent_visibility) { :private } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is direct' do + let(:parent_visibility) { :direct } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + + context 'when signed request account is domain blocked' do + before do + status.account.block_domain!(remote_account.domain) + get :index, params: { account_username: status.account.username, status_id: status.id } + end + + context 'when status is public' do + let(:parent_visibility) { :public } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is private' do + let(:parent_visibility) { :private } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is direct' do + let(:parent_visibility) { :direct } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + end + end +end diff --git a/spec/controllers/statuses_controller_spec.rb b/spec/controllers/statuses_controller_spec.rb index 6905dae10e3..ba1f1370a63 100644 --- a/spec/controllers/statuses_controller_spec.rb +++ b/spec/controllers/statuses_controller_spec.rb @@ -5,128 +5,821 @@ require 'rails_helper' describe StatusesController do render_views - describe '#show' do - context 'account is suspended' do - it 'returns gone' do - account = Fabricate(:account, suspended: true) - status = Fabricate(:status, account: account) + describe 'GET #show' do + let(:account) { Fabricate(:account) } + let(:status) { Fabricate(:status, account: account) } + context 'when account is suspended' do + let(:account) { Fabricate(:account, suspended: true) } + + before do get :show, params: { account_username: account.username, id: status.id } + end + it 'returns http gone' do expect(response).to have_http_status(410) end end - context 'status is not permitted' do - it 'raises ActiveRecord::RecordNotFound' do - user = Fabricate(:user) - status = Fabricate(:status) - status.account.block!(user.account) + context 'when status is a reblog' do + let(:original_account) { Fabricate(:account, domain: 'example.com') } + let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') } + let(:status) { Fabricate(:status, account: account, reblog: original_status) } - sign_in(user) + before do get :show, params: { account_username: status.account.username, id: status.id } - - expect(response).to have_http_status(404) end - end - context 'status is a reblog' do it 'redirects to the original status' do - original_account = Fabricate(:account, domain: 'example.com') - original_status = Fabricate(:status, account: original_account, uri: 'tag:example.com,2017:foo', url: 'https://example.com/123') - status = Fabricate(:status, reblog: original_status) - - get :show, params: { account_username: status.account.username, id: status.id } - expect(response).to redirect_to(original_status.url) end end - context 'account is not suspended and status is permitted' do - it 'assigns @account' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:account)).to eq status.account + context 'when status is public' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } end - it 'assigns @status' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:status)).to eq status + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end end - it 'assigns @ancestors for ancestors of the status if it is a reply' do - ancestor = Fabricate(:status) - status = Fabricate(:status, in_reply_to_id: ancestor.id) + context 'as JSON' do + let(:format) { 'json' } - get :show, params: { account_username: status.account.username, id: status.id } + it 'returns http success' do + expect(response).to have_http_status(200) + end - expect(assigns(:ancestors)).to eq [ancestor] + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when status is private' do + let(:status) { Fabricate(:status, account: account, visibility: :private) } + + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } end - it 'assigns @ancestors for [] if it is not a reply' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(assigns(:ancestors)).to eq [] + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end end - it 'assigns @descendant_threads for a thread with several statuses' do - status = Fabricate(:status) - child = Fabricate(:status, in_reply_to_id: status.id) - grandchild = Fabricate(:status, in_reply_to_id: child.id) + context 'as HTML' do + let(:format) { 'html' } - get :show, params: { account_username: status.account.username, id: status.id } + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end - expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchild.id] + context 'when status is direct' do + let(:status) { Fabricate(:status, account: account, visibility: :direct) } + + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } end - it 'assigns @descendant_threads for several threads sharing the same descendant' do - status = Fabricate(:status) - child = Fabricate(:status, in_reply_to_id: status.id) - grandchildren = 2.times.map { Fabricate(:status, in_reply_to_id: child.id) } + context 'as JSON' do + let(:format) { 'json' } - get :show, params: { account_username: status.account.username, id: status.id } - - expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).to eq [child.id, grandchildren[0].id] - expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).to eq [grandchildren[1].id] + it 'returns http not found' do + expect(response).to have_http_status(404) + end end - it 'assigns @max_descendant_thread_id for the last thread if it is hitting the status limit' do - stub_const 'StatusControllerConcern::DESCENDANTS_LIMIT', 1 - status = Fabricate(:status) - child = Fabricate(:status, in_reply_to_id: status.id) + context 'as HTML' do + let(:format) { 'html' } - get :show, params: { account_username: status.account.username, id: status.id } + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end - expect(assigns(:descendant_threads)).to eq [] - expect(assigns(:max_descendant_thread_id)).to eq child.id + context 'when signed-in' do + let(:user) { Fabricate(:user) } + + before do + sign_in(user) end - it 'assigns @descendant_threads for threads with :next_status key if they are hitting the depth limit' do - stub_const 'StatusControllerConcern::DESCENDANTS_DEPTH_LIMIT', 2 - status = Fabricate(:status) - child0 = Fabricate(:status, in_reply_to_id: status.id) - child1 = Fabricate(:status, in_reply_to_id: child0.id) - child2 = Fabricate(:status, in_reply_to_id: child0.id) + context 'when account blocks user' do + before do + account.block!(user.account) + get :show, params: { account_username: status.account.username, id: status.id } + end - get :show, params: { account_username: status.account.username, id: status.id } - - expect(assigns(:descendant_threads)[0][:statuses].pluck(:id)).not_to include child1.id - expect(assigns(:descendant_threads)[1][:statuses].pluck(:id)).not_to include child2.id - expect(assigns(:descendant_threads)[0][:next_status].id).to eq child1.id - expect(assigns(:descendant_threads)[1][:next_status].id).to eq child2.id + it 'returns http not found' do + expect(response).to have_http_status(404) + end end - it 'returns a success' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } + context 'when status is public' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when status is private' do + let(:status) { Fabricate(:status, account: account, visibility: :private) } + + context 'when user is authorized to see it' do + before do + user.account.follow!(account) + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when user is not authorized to see it' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + end + + context 'when status is direct' do + let(:status) { Fabricate(:status, account: account, visibility: :direct) } + + context 'when user is authorized to see it' do + before do + Fabricate(:mention, account: user.account, status: status) + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when user is not authorized to see it' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + end + end + + context 'with signature' do + let(:remote_account) { Fabricate(:account, domain: 'example.com') } + + before do + allow(controller).to receive(:signed_request_account).and_return(remote_account) + end + + context 'when account blocks account' do + before do + account.block!(remote_account) + get :show, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when account domain blocks account' do + before do + account.block_domain!(remote_account.domain) + get :show, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is public' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when status is private' do + let(:status) { Fabricate(:status, account: account, visibility: :private) } + + context 'when user is authorized to see it' do + before do + remote_account.follow!(account) + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when user is not authorized to see it' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + end + + context 'when status is direct' do + let(:status) { Fabricate(:status, account: account, visibility: :direct) } + + context 'when user is authorized to see it' do + before do + Fabricate(:mention, account: remote_account, status: status) + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns no Cache-Control header' do + expect(response.headers).to_not include 'Cache-Control' + end + + it 'renders status' do + expect(response).to render_template(:show) + expect(response.body).to include status.text + end + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http success' do + expect(response).to have_http_status(200) + end + + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns private Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'private' + end + + it 'returns Content-Type header' do + expect(response.headers['Content-Type']).to include 'application/activity+json' + end + + it 'renders ActivityPub Note object' do + json = body_as_json + expect(json[:content]).to include status.text + end + end + end + + context 'when user is not authorized to see it' do + before do + get :show, params: { account_username: status.account.username, id: status.id, format: format } + end + + context 'as JSON' do + let(:format) { 'json' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'as HTML' do + let(:format) { 'html' } + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + end + end + end + end + + describe 'GET #activity' do + let(:account) { Fabricate(:account) } + let(:status) { Fabricate(:status, account: account) } + + context 'when account is suspended' do + let(:account) { Fabricate(:account, suspended: true) } + + before do + get :activity, params: { account_username: account.username, id: status.id } + end + + it 'returns http gone' do + expect(response).to have_http_status(410) + end + end + + context 'when status is public' do + pending + end + + context 'when status is private' do + pending + end + + context 'when status is direct' do + pending + end + + context 'when signed-in' do + context 'when status is public' do + pending + end + + context 'when status is private' do + context 'when user is authorized to see it' do + pending + end + + context 'when user is not authorized to see it' do + pending + end + end + + context 'when status is direct' do + context 'when user is authorized to see it' do + pending + end + + context 'when user is not authorized to see it' do + pending + end + end + end + + context 'with signature' do + context 'when status is public' do + pending + end + + context 'when status is private' do + context 'when user is authorized to see it' do + pending + end + + context 'when user is not authorized to see it' do + pending + end + end + + context 'when status is direct' do + context 'when user is authorized to see it' do + pending + end + + context 'when user is not authorized to see it' do + pending + end + end + end + end + + describe 'GET #embed' do + let(:account) { Fabricate(:account) } + let(:status) { Fabricate(:status, account: account) } + + context 'when account is suspended' do + let(:account) { Fabricate(:account, suspended: true) } + + before do + get :embed, params: { account_username: account.username, id: status.id } + end + + it 'returns http gone' do + expect(response).to have_http_status(410) + end + end + + context 'when status is a reblog' do + let(:original_account) { Fabricate(:account, domain: 'example.com') } + let(:original_status) { Fabricate(:status, account: original_account, url: 'https://example.com/123') } + let(:status) { Fabricate(:status, account: account, reblog: original_status) } + + before do + get :embed, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is public' do + before do + get :embed, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http success' do expect(response).to have_http_status(200) end - it 'renders statuses/show' do - status = Fabricate(:status) - get :show, params: { account_username: status.account.username, id: status.id } - expect(response).to render_template 'statuses/show' + it 'returns Link header' do + expect(response.headers['Link'].to_s).to include 'activity+json' + end + + it 'returns Vary header' do + expect(response.headers['Vary']).to eq 'Accept' + end + + it 'returns public Cache-Control header' do + expect(response.headers['Cache-Control']).to include 'public' + end + + it 'renders status' do + expect(response).to render_template(:embed) + expect(response.body).to include status.text + end + end + + context 'when status is private' do + let(:status) { Fabricate(:status, account: account, visibility: :private) } + + before do + get :embed, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) + end + end + + context 'when status is direct' do + let(:status) { Fabricate(:status, account: account, visibility: :direct) } + + before do + get :embed, params: { account_username: status.account.username, id: status.id } + end + + it 'returns http not found' do + expect(response).to have_http_status(404) end end end diff --git a/spec/fabricators/status_pin_fabricator.rb b/spec/fabricators/status_pin_fabricator.rb index 6a9006c9fb2..f1f1c05f38d 100644 --- a/spec/fabricators/status_pin_fabricator.rb +++ b/spec/fabricators/status_pin_fabricator.rb @@ -1,4 +1,4 @@ Fabricator(:status_pin) do account - status + status { |attrs| Fabricate(:status, account: attrs[:account], visibility: :public) } end diff --git a/spec/models/status_spec.rb b/spec/models/status_spec.rb index 38537da448d..02f5332870a 100644 --- a/spec/models/status_spec.rb +++ b/spec/models/status_spec.rb @@ -96,20 +96,16 @@ RSpec.describe Status, type: :model do context 'unless destroyed?' do context 'if reblog?' do - it 'returns "#{account.acct} shared #{reblog.account.acct}\'s: #{preview}"' do + it 'returns "#{account.acct} shared a status by #{reblog.account.acct}"' do reblog = subject.reblog = other - preview = subject.text.slice(0, 10).split("\n")[0] - expect(subject.title).to( - eq "#{account.acct} shared #{reblog.account.acct}'s: #{preview}" - ) + expect(subject.title).to eq "#{account.acct} shared a status by #{reblog.account.acct}" end end context 'unless reblog?' do - it 'returns "#{account.acct}: #{preview}"' do + it 'returns "New status by #{account.acct}"' do subject.reblog = nil - preview = subject.text.slice(0, 20).split("\n")[0] - expect(subject.title).to eq "#{account.acct}: #{preview}" + expect(subject.title).to eq "New status by #{account.acct}" end end end diff --git a/spec/services/fetch_resource_service_spec.rb b/spec/services/fetch_resource_service_spec.rb index 3af6a06899e..ded05ffbc70 100644 --- a/spec/services/fetch_resource_service_spec.rb +++ b/spec/services/fetch_resource_service_spec.rb @@ -21,7 +21,11 @@ RSpec.describe FetchResourceService, type: :service do context 'when OpenSSL::SSL::SSLError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(OpenSSL::SSL::SSLError) + request = double() + allow(Request).to receive(:new).and_return(request) + allow(request).to receive(:add_headers) + allow(request).to receive(:on_behalf_of) + allow(request).to receive(:perform).and_raise(OpenSSL::SSL::SSLError) end it { is_expected.to be_nil } @@ -29,7 +33,11 @@ RSpec.describe FetchResourceService, type: :service do context 'when HTTP::ConnectionError is raised' do before do - allow(Request).to receive_message_chain(:new, :add_headers, :on_behalf_of, :perform).and_raise(HTTP::ConnectionError) + request = double() + allow(Request).to receive(:new).and_return(request) + allow(request).to receive(:add_headers) + allow(request).to receive(:on_behalf_of) + allow(request).to receive(:perform).and_raise(HTTP::ConnectionError) end it { is_expected.to be_nil } diff --git a/yarn.lock b/yarn.lock index 2d57ec0babf..5715672cb8e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1327,9 +1327,9 @@ integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== "@types/q@^1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" - integrity sha512-eqz8c/0kwNi/OEHQfvIuJVLTst3in0e7uTKeuY+WL/zfKn0xVujOTp42bS/vUUokhK5P2BppLd9JXMOMHcgbjA== + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== "@types/react@16.4.6": version "16.4.6" @@ -1632,7 +1632,7 @@ ajv@^4.7.0: co "^4.6.0" json-stable-stringify "^1.0.1" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5, ajv@^6.9.1: +ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.9.1: version "6.10.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== @@ -1652,6 +1652,16 @@ ajv@^6.12.0: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^6.5.5: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + alphanum-sort@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" @@ -1971,9 +1981,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== axios@^0.18.0: version "0.18.1" @@ -2249,9 +2259,9 @@ bindings@^1.5.0: file-uri-to-path "1.0.0" bluebird@^3.5.5: - version "3.5.5" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" - integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== blurhash@^1.1.3: version "1.1.3" @@ -2461,11 +2471,6 @@ buffer@^4.3.0: ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= - builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" @@ -2482,9 +2487,9 @@ bytes@3.1.0: integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== cacache@^12.0.2, cacache@^12.0.3: - version "12.0.3" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" - integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== + version "12.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.4.tgz#668bcbd105aeb5f1d92fe25570ec9525c8faa40c" + integrity sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ== dependencies: bluebird "^3.5.5" chownr "^1.1.1" @@ -2703,16 +2708,11 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" -chownr@^1.1.1: +chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== -chownr@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.3.tgz#42d837d5239688d55f303003a508230fa6727142" - integrity sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw== - chrome-trace-event@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" @@ -2818,7 +2818,7 @@ co@^4.6.0: resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= -coa@~2.0.1: +coa@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/coa/-/coa-2.0.2.tgz#43f6c21151b4ef2bf57187db0d73de229e3e7ec3" integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== @@ -2880,15 +2880,10 @@ color@^3.0.0: color-convert "^1.9.1" color-string "^1.5.2" -colors@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" - integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= - combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" - integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w== + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" @@ -3052,9 +3047,9 @@ core-js-compat@^3.6.2: semver "7.0.0" core-js-pure@^3.0.0: - version "3.6.4" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a" - integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw== + version "3.6.5" + resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" + integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== core-js@^2.4.0: version "2.6.1" @@ -3235,18 +3230,18 @@ css-loader@^3.4.2: postcss-value-parser "^4.0.2" schema-utils "^2.6.0" -css-select-base-adapter@~0.1.0: +css-select-base-adapter@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7" integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== css-select@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.0.2.tgz#ab4386cec9e1f668855564b17c3733b43b2a5ede" - integrity sha512-dSpYaDVoWaELjvZ3mS6IKZM/y2PMPa/XYoEfYNZePL4U/XgyxZNroHEHReDx/d+VgXh9VbCTtFqLkFbmeqeaRQ== + version "2.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-2.1.0.tgz#6a34653356635934a81baca68d0255432105dbef" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== dependencies: boolbase "^1.0.0" - css-what "^2.1.2" + css-what "^3.2.1" domutils "^1.7.0" nth-check "^1.0.2" @@ -3265,37 +3260,37 @@ css-system-font-keywords@^1.0.0: resolved "https://registry.yarnpkg.com/css-system-font-keywords/-/css-system-font-keywords-1.0.0.tgz#85c6f086aba4eb32c571a3086affc434b84823ed" integrity sha1-hcbwhquk6zLFcaMIav/ENLhII+0= -css-tree@1.0.0-alpha.28: - version "1.0.0-alpha.28" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.28.tgz#8e8968190d886c9477bc8d61e96f61af3f7ffa7f" - integrity sha512-joNNW1gCp3qFFzj4St6zk+Wh/NBv0vM5YbEreZk0SD4S23S+1xBKb6cLDg2uj4P4k/GUMlIm6cKIDqIG+vdt0w== +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.37.tgz#98bebd62c4c1d9f960ec340cf9f7522e30709a22" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== dependencies: - mdn-data "~1.1.0" - source-map "^0.5.3" + mdn-data "2.0.4" + source-map "^0.6.1" -css-tree@1.0.0-alpha.29: - version "1.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.29.tgz#3fa9d4ef3142cbd1c301e7664c1f352bd82f5a39" - integrity sha512-sRNb1XydwkW9IOci6iB2xmy8IGCj6r/fr+JWitvJ2JxQRPzN3T4AGGVWCMlVmVwM1gtgALJRmGIlWv5ppnGGkg== +css-tree@1.0.0-alpha.39: + version "1.0.0-alpha.39" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.0.0-alpha.39.tgz#2bff3ffe1bb3f776cf7eefd91ee5cba77a149eeb" + integrity sha512-7UvkEYgBAHRG9Nt980lYxjsTrCyHFN53ky3wVsDkiMdVqylqRt+Zc+jm5qw7/qyOvN2dHSYtX0e4MbCCExSvnA== dependencies: - mdn-data "~1.1.0" - source-map "^0.5.3" + mdn-data "2.0.6" + source-map "^0.6.1" css-unit-converter@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.1.tgz#d9b9281adcfd8ced935bdbaba83786897f64e996" integrity sha1-2bkoGtz9jO2TW9urqDeGiX9k6ZY= -css-url-regex@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/css-url-regex/-/css-url-regex-1.1.0.tgz#83834230cc9f74c457de59eebd1543feeb83b7ec" - integrity sha1-g4NCMMyfdMRX3lnuvRVD/uuDt+w= - -css-what@2.1, css-what@^2.1.2: +css-what@2.1: version "2.1.3" resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css-what@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.2.1.tgz#f4a8f12421064621b456755e34a03a2c22df5da1" + integrity sha512-WwOrosiQTvyms+Ti5ZC5vGEK0Vod3FTt1ca+payZqvKuGJF+dq7bG63DstxtN0dpm6FxY27a/zS3Wten+gEtGw== + cssesc@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703" @@ -3374,12 +3369,12 @@ cssnano@^4.1.10: is-resolvable "^1.0.0" postcss "^7.0.0" -csso@^3.5.0: - version "3.5.1" - resolved "https://registry.yarnpkg.com/csso/-/csso-3.5.1.tgz#7b9eb8be61628973c1b261e169d2f024008e758b" - integrity sha512-vrqULLffYU1Q2tLdJvaCYbONStnfkfimRxXNaGjxMldI0C7JPBC4rB1RyjhfdZ4m1frm8pM9uRPKH3d2knZ8gg== +csso@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.0.3.tgz#0d9985dc852c7cc2b2cacfbbe1079014d1a8e903" + integrity sha512-NL3spysxUkcrOgnpsT4Xdl2aiEiBG6bXswAABQVHcMrfjjBisFOKwLDOmf4wf32aPdcJws1zds2B0Rg+jqMyHQ== dependencies: - css-tree "1.0.0-alpha.29" + css-tree "1.0.0-alpha.39" cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": version "0.3.8" @@ -3961,7 +3956,7 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.1.1" -es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5, es-abstract@^1.5.1: +es-abstract@^1.13.0, es-abstract@^1.17.0, es-abstract@^1.17.0-next.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.5: version "1.17.5" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== @@ -4524,9 +4519,9 @@ fast-deep-equal@^3.1.1: integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== fast-levenshtein@~2.0.6: version "2.0.6" @@ -4560,9 +4555,9 @@ fb-watchman@^2.0.0: bser "^2.0.0" figgy-pudding@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" - integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== + version "3.5.2" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" + integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== figures@^1.3.5: version "1.7.0" @@ -4722,9 +4717,9 @@ flat-cache@^2.0.1: write "1.0.3" flatted@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.0.tgz#55122b6536ea496b4b44893ee2608141d10d9916" - integrity sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" + integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== flush-write-stream@^1.0.0: version "1.1.1" @@ -4890,9 +4885,9 @@ functional-red-black-tree@^1.0.1: integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= functions-have-names@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.0.tgz#83da7583e4ea0c9ac5ff530f73394b033e0bf77d" - integrity sha512-zKXyzksTeaCSw5wIX79iCA40YAa6CJMJgNg9wdkU/ERBrIdPSimPICYiLp65lRbSBqtiHql/HZfS2DyI/AH6tQ== + version "1.2.1" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.1.tgz#a981ac397fa0c9964551402cdc5533d7a4d52f91" + integrity sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA== gauge@~2.7.3: version "2.7.4" @@ -5119,7 +5114,7 @@ har-schema@^2.0.0: resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== @@ -5275,9 +5270,9 @@ hoopy@^0.1.4: integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== hosted-git-info@^2.1.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" - integrity sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w== + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== hpack.js@^2.1.6: version "2.1.6" @@ -5759,13 +5754,6 @@ is-buffer@^2.0.2: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - integrity sha1-VAVy0096wxGfj3bDDLwbHgN6/74= - dependencies: - builtin-modules "^1.0.0" - is-callable@^1.1.4, is-callable@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" @@ -6561,7 +6549,7 @@ js-string-escape@1.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0: +js-yaml@^3.13.1, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.9.0: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -6928,9 +6916,9 @@ lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== loglevel@^1.6.6: - version "1.6.6" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.6.tgz#0ee6300cc058db6b3551fa1c4bf73b83bb771312" - integrity sha512-Sgr5lbboAUBo3eXCSPL4/KoVz3ROKquOjcctxmHIt+vol2DrqTQe3SwkKKuYhEiWB5kYa13YyopJ69deJ1irzQ== + version "1.6.8" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" + integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" @@ -7013,10 +7001,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -mdn-data@~1.1.0: - version "1.1.4" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01" - integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA== +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.4.tgz#699b3c38ac6f1d728091a64650b65d388502fd5b" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +mdn-data@2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978" + integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA== media-typer@0.3.0: version "0.3.0" @@ -7100,34 +7093,22 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== "mime-db@>= 1.40.0 < 2": version "1.42.0" resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg== - -mime-types@^2.1.12, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" - integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg== +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "~1.37.0" - -mime-types@~2.1.17, mime-types@~2.1.24: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" + mime-db "1.44.0" mime@1.6.0: version "1.6.0" @@ -7253,14 +7234,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5, mkdirp@^0.5.3, mkdirp@~0.5.1: - version "0.5.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" - integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== - dependencies: - minimist "^1.2.5" - -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@~0.5.1: version "0.5.5" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== @@ -7272,10 +7246,10 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -moo@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e" - integrity sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw== +moo@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/moo/-/moo-0.5.1.tgz#7aae7f384b9b09f620b6abf6f74ebbcd1b65dbc4" + integrity sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w== mousetrap@^1.5.2: version "1.6.5" @@ -7360,12 +7334,12 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nearley@^2.7.10: - version "2.16.0" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.16.0.tgz#77c297d041941d268290ec84b739d0ee297e83a7" - integrity sha512-Tr9XD3Vt/EujXbZBv6UAHYoLUSMQAxSsTnm9K3koXzjzNWY195NqALeyrzLZBKzAkL3gl92BcSogqrHjD8QuUg== + version "2.19.2" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.2.tgz#40cafbf235121ae94b1aa1e585890d24fade182d" + integrity sha512-h6lygT0BWAGErDvoE2LfI+tDeY2+UUrqG5dcBPdCmjnjud9z1wE0P7ljb85iNbE93YA+xJLpoSYGMuUqhnSSSA== dependencies: commander "^2.19.0" - moo "^0.4.3" + moo "^0.5.0" railroad-diagrams "^1.0.0" randexp "0.4.6" semver "^5.4.1" @@ -7484,12 +7458,12 @@ nopt@^4.0.1: osenv "^0.1.4" normalize-package-data@^2.3.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - integrity sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw== + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" + resolve "^1.10.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" @@ -7666,13 +7640,13 @@ object.fromentries@^2.0.2: function-bind "^1.1.1" has "^1.0.3" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" - integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= +object.getownpropertydescriptors@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== dependencies: - define-properties "^1.1.2" - es-abstract "^1.5.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" object.pick@^1.3.0: version "1.3.0" @@ -7681,7 +7655,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.0.4, object.values@^1.1.0, object.values@^1.1.1: +object.values@^1.1.0, object.values@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== @@ -8745,10 +8719,10 @@ prr@~1.0.1: resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= -psl@^1.1.24, psl@^1.1.28: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== public-encrypt@^4.0.0: version "4.0.3" @@ -8792,7 +8766,7 @@ punycode@1.3.2: resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= -punycode@^1.2.4, punycode@^1.4.1: +punycode@^1.2.4: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= @@ -9223,10 +9197,10 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -9236,10 +9210,10 @@ read-pkg@^3.0.0: string_decoder "~1.1.1" util-deprecate "~1.0.1" -readable-stream@^2.0.2, readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== +readable-stream@^2.0.1, readable-stream@^2.3.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -9493,9 +9467,9 @@ request-promise-native@^1.0.5: tough-cookie ">=2.3.3" request@^2.87.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" - integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -9504,7 +9478,7 @@ request@^2.87.0: extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.0" + har-validator "~5.1.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -9514,7 +9488,7 @@ request@^2.87.0: performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" - tough-cookie "~2.4.3" + tough-cookie "~2.5.0" tunnel-agent "^0.6.0" uuid "^3.3.2" @@ -9621,10 +9595,10 @@ resolve@1.1.7: resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" - integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.15.1, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.8.1: + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" @@ -10168,7 +10142,7 @@ source-map@0.5.6: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= @@ -10187,9 +10161,9 @@ spdx-correct@^3.0.0: spdx-license-ids "^3.0.0" spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: version "3.0.0" @@ -10200,9 +10174,9 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - integrity sha512-uBIcIl3Ih6Phe3XHK1NqboJLdGfwr1UN3k6wSD1dZpmPsIkb8AGNbZYJ1fOBk834+Gxy8rpfDxrS6XLEMZMY2g== + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== spdy-transport@^3.0.0: version "3.0.0" @@ -10276,7 +10250,7 @@ ssri@^7.0.0: figgy-pudding "^3.5.1" minipass "^3.1.1" -stable@~0.1.6: +stable@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== @@ -10590,22 +10564,21 @@ supports-color@^7.0.0, supports-color@^7.1.0: has-flag "^4.0.0" svgo@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.1.1.tgz#12384b03335bcecd85cfa5f4e3375fed671cb985" - integrity sha512-GBkJbnTuFpM4jFbiERHDWhZc/S/kpHToqmZag3aEBjPYK44JAN2QBjvrGIxLOoCyMZjuFQIfTO2eJd8uwLY/9g== + version "1.3.2" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== dependencies: - coa "~2.0.1" - colors "~1.1.2" + chalk "^2.4.1" + coa "^2.0.2" css-select "^2.0.0" - css-select-base-adapter "~0.1.0" - css-tree "1.0.0-alpha.28" - css-url-regex "^1.1.0" - csso "^3.5.0" - js-yaml "^3.12.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" mkdirp "~0.5.1" - object.values "^1.0.4" + object.values "^1.1.0" sax "~1.2.4" - stable "~0.1.6" + stable "^0.1.8" unquote "~1.1.1" util.promisify "~1.0.0" @@ -10873,7 +10846,7 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -tough-cookie@>=2.3.3, tough-cookie@^2.3.4: +tough-cookie@>=2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== @@ -10881,14 +10854,6 @@ tough-cookie@>=2.3.3, tough-cookie@^2.3.4: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" @@ -11104,12 +11069,14 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1: integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@^1.0.0, util.promisify@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - integrity sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA== + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" util@0.10.3: version "0.10.3"