~cytrogen/masto-fe

0dfc6ea3effd8ee118c6b23b0a70588736ca33e6 — Claire 2 years ago 3378bdb + 882e770
Merge pull request #2365 from ClearlyClaire/glitch-soc/merge-upstream

Merge upstream up to 425d77f8124a50fc033e8fb3bdf7b89a6a25f4fa
131 files changed, 1819 insertions(+), 1186 deletions(-)

M .github/workflows/test-ruby.yml
M .rubocop.yml
M .rubocop_todo.yml
M CHANGELOG.md
M Gemfile
M Gemfile.lock
M app/controllers/accounts_controller.rb
M app/controllers/admin/instances_controller.rb
M app/controllers/api/base_controller.rb
M app/controllers/api/v1/instances/activity_controller.rb
M app/controllers/api/v1/instances/domain_blocks_controller.rb
M app/controllers/api/v1/instances/extended_descriptions_controller.rb
M app/controllers/api/v1/instances/peers_controller.rb
M app/controllers/api/v1/instances/privacy_policies_controller.rb
M app/controllers/api/v1/instances/rules_controller.rb
M app/controllers/api/v1/instances/translation_languages_controller.rb
M app/controllers/api/v1/instances_controller.rb
M app/controllers/api/v1/peers/search_controller.rb
M app/controllers/application_controller.rb
M app/controllers/concerns/account_owned_concern.rb
M app/controllers/concerns/api_caching_concern.rb
M app/controllers/follower_accounts_controller.rb
M app/controllers/following_accounts_controller.rb
M app/controllers/mail_subscriptions_controller.rb
M app/controllers/media_controller.rb
M app/controllers/media_proxy_controller.rb
M app/controllers/statuses_controller.rb
M app/controllers/tags_controller.rb
M app/helpers/domain_control_helper.rb
M app/helpers/languages_helper.rb
M app/javascript/flavours/glitch/features/interaction_modal/index.jsx
A app/javascript/mastodon/components/badge.jsx
M app/javascript/mastodon/features/account/components/header.jsx
M app/javascript/mastodon/features/interaction_modal/index.jsx
M app/javascript/mastodon/features/public_timeline/index.jsx
M app/javascript/mastodon/features/status/components/card.jsx
M app/javascript/styles/mastodon/accounts.scss
M app/javascript/styles/mastodon/basics.scss
M app/javascript/styles/mastodon/components.scss
M app/javascript/styles/mastodon/variables.scss
M app/lib/importer/base_importer.rb
M app/lib/request.rb
M app/lib/rss/builder.rb
M app/lib/text_formatter.rb
M app/mailers/notification_mailer.rb
M app/models/media_attachment.rb
M app/serializers/initial_state_serializer.rb
M app/services/concerns/payloadable.rb
M app/services/fetch_link_card_service.rb
M app/services/unallow_domain_service.rb
M app/validators/language_validator.rb
M app/validators/status_length_validator.rb
M app/validators/url_validator.rb
M app/views/admin/instances/index.html.haml
M app/views/admin/instances/show.html.haml
M app/views/layouts/mailer.html.haml
M app/views/well_known/host_meta/show.xml.ruby
M config/application.rb
M config/brakeman.ignore
A config/brakeman.yml
M config/imagemagick/policy.xml
A config/initializers/2_limited_federation_mode.rb
D config/initializers/2_whitelist_mode.rb
M config/initializers/twitter_regex.rb
M config/navigation.rb
M config/webpack/rules/index.js
A config/webpack/rules/material_icons.js
M config/webpack/tests.js
M db/migrate/20180812173710_copy_status_stats.rb
M db/migrate/20181116173541_copy_account_stats.rb
M docker-compose.yml
D lib/action_controller/conditional_get_extensions.rb
M lib/mastodon/cli/statuses.rb
M lib/mastodon/version.rb
M lib/paperclip/media_type_spoof_detector_extensions.rb
M lib/tasks/mastodon.rake
A lib/tasks/spec.rake
M package.json
M spec/controllers/concerns/signature_verification_spec.rb
R spec/{ => fabricators}/fabricators_spec.rb
A spec/fixtures/files/600x400.avif
A spec/fixtures/files/600x400.heic
A spec/fixtures/files/600x400.jpeg
A spec/fixtures/files/600x400.png
A spec/fixtures/files/600x400.webp
D spec/fixtures/requests/idn.txt
D spec/helpers/admin/action_logs_helper_spec.rb
M spec/lib/request_spec.rb
M spec/mailers/notification_mailer_spec.rb
D spec/models/account_alias_spec.rb
D spec/models/account_deletion_request_spec.rb
D spec/models/account_moderation_note_spec.rb
D spec/models/announcement_mute_spec.rb
D spec/models/announcement_reaction_spec.rb
D spec/models/announcement_spec.rb
D spec/models/backup_spec.rb
D spec/models/conversation_mute_spec.rb
D spec/models/custom_filter_keyword_spec.rb
D spec/models/custom_filter_spec.rb
D spec/models/device_spec.rb
D spec/models/encrypted_message_spec.rb
D spec/models/featured_tag_spec.rb
D spec/models/follow_recommendation_suppression_spec.rb
D spec/models/list_account_spec.rb
D spec/models/list_spec.rb
D spec/models/login_activity_spec.rb
M spec/models/media_attachment_spec.rb
D spec/models/mute_spec.rb
D spec/models/preview_card_spec.rb
D spec/models/preview_card_trend_spec.rb
D spec/models/relay_spec.rb
D spec/models/scheduled_status_spec.rb
D spec/models/status_stat_spec.rb
D spec/models/status_trend_spec.rb
D spec/models/system_key_spec.rb
D spec/models/tag_follow_spec.rb
D spec/models/unavailable_domain_spec.rb
D spec/models/user_invite_request_spec.rb
D spec/models/web/setting_spec.rb
M spec/rails_helper.rb
M spec/requests/cache_spec.rb
A spec/requests/mail_subscriptions_spec.rb
M spec/services/fetch_link_card_service_spec.rb
M spec/services/unallow_domain_service_spec.rb
D spec/services/unmute_service_spec.rb
M spec/spec_helper.rb
M spec/support/stories/profile_stories.rb
A spec/system/new_statuses_spec.rb
A spec/validators/language_validator_spec.rb
M spec/validators/url_validator_spec.rb
M yarn.lock
M .github/workflows/test-ruby.yml => .github/workflows/test-ruby.yml +97 -0
@@ 153,3 153,100 @@ jobs:
        run: './bin/rails db:create db:schema:load db:seed'

      - run: bundle exec rake rspec_chunked

  test-e2e:
    name: End to End testing
    runs-on: ubuntu-latest

    needs:
      - build

    services:
      postgres:
        image: postgres:14-alpine
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_USER: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    env:
      DB_HOST: localhost
      DB_USER: postgres
      DB_PASS: postgres
      DISABLE_SIMPLECOV: true
      RAILS_ENV: test
      BUNDLE_WITH: test

    strategy:
      fail-fast: false
      matrix:
        ruby-version:
          - '3.0'
          - '3.1'
          - '.ruby-version'

    steps:
      - uses: actions/checkout@v3

      - uses: actions/download-artifact@v3
        with:
          path: './public'
          name: ${{ github.sha }}

      - name: Update package index
        run: sudo apt-get update

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          cache: yarn
          node-version-file: '.nvmrc'

      - name: Install native Ruby dependencies
        run: sudo apt-get install -y libicu-dev libidn11-dev

      - name: Install additional system dependencies
        run: sudo apt-get install -y ffmpeg imagemagick

      - name: Set up bundler cache
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby-version}}
          bundler-cache: true

      - run: yarn --frozen-lockfile

      - name: Load database schema
        run: './bin/rails db:create db:schema:load db:seed'

      - run: bundle exec rake spec:system

      - name: Archive logs
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: e2e-logs-${{ matrix.ruby-version }}
          path: log/

      - name: Archive test screenshots
        uses: actions/upload-artifact@v3
        if: failure()
        with:
          name: e2e-screenshots
          path: tmp/screenshots/

M .rubocop.yml => .rubocop.yml +1 -8
@@ 38,14 38,7 @@ Layout/FirstHashElementIndentation:
# Reason: Currently disabled in .rubocop_todo.yml
# https://docs.rubocop.org/rubocop/cops_layout.html#layoutlinelength
Layout/LineLength:
  AllowedPatterns:
    # Allow comments to be long lines
    - !ruby/regexp / \# .*$/
    - !ruby/regexp /^\# .*$/
  Exclude:
    - 'lib/mastodon/cli/*.rb'
    - db/*migrate/**/*
    - db/seeds/**/*
  Max: 320 # Default of 120 causes a duplicate entry in generated todo file

# Reason:
# https://docs.rubocop.org/rubocop/cops_lint.html#lintuselessaccessmodifier

M .rubocop_todo.yml => .rubocop_todo.yml +9 -89
@@ 40,6 40,13 @@ Layout/LeadingCommentSpace:
    - 'config/initializers/omniauth.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
Layout/LineLength:
  Exclude:
    - 'app/models/account.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: require_no_space, require_space
Layout/SpaceInLambdaLiteral:


@@ 112,7 119,6 @@ Lint/UselessAssignment:
    - 'config/initializers/omniauth.rb'
    - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
    - 'db/post_migrate/20190511152737_remove_suspended_silenced_account_fields.rb'
    - 'spec/controllers/api/v1/bookmarks_controller_spec.rb'
    - 'spec/controllers/api/v1/favourites_controller_spec.rb'
    - 'spec/controllers/concerns/account_controller_concern_spec.rb'
    - 'spec/helpers/jsonld_helper_spec.rb'


@@ 129,7 135,7 @@ Lint/UselessAssignment:

# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
  Max: 150
  Max: 143
  Exclude:
    - 'app/serializers/initial_state_serializer.rb'



@@ 160,14 166,6 @@ Naming/VariableNumber:
    - 'spec/models/domain_block_spec.rb'
    - 'spec/models/user_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
Performance/UnfreezeString:
  Exclude:
    - 'app/lib/rss/builder.rb'
    - 'app/lib/text_formatter.rb'
    - 'app/validators/status_length_validator.rb'
    - 'lib/tasks/mastodon.rake'

RSpec/AnyInstance:
  Exclude:
    - 'spec/controllers/activitypub/inboxes_controller_spec.rb'


@@ 187,41 185,6 @@ RSpec/AnyInstance:
    - 'spec/workers/activitypub/delivery_worker_spec.rb'
    - 'spec/workers/web/push_notification_worker_spec.rb'

# This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/EmptyExampleGroup:
  Exclude:
    - 'spec/helpers/admin/action_logs_helper_spec.rb'
    - 'spec/models/account_alias_spec.rb'
    - 'spec/models/account_deletion_request_spec.rb'
    - 'spec/models/account_moderation_note_spec.rb'
    - 'spec/models/announcement_mute_spec.rb'
    - 'spec/models/announcement_reaction_spec.rb'
    - 'spec/models/announcement_spec.rb'
    - 'spec/models/backup_spec.rb'
    - 'spec/models/conversation_mute_spec.rb'
    - 'spec/models/custom_filter_keyword_spec.rb'
    - 'spec/models/custom_filter_spec.rb'
    - 'spec/models/device_spec.rb'
    - 'spec/models/encrypted_message_spec.rb'
    - 'spec/models/featured_tag_spec.rb'
    - 'spec/models/follow_recommendation_suppression_spec.rb'
    - 'spec/models/list_account_spec.rb'
    - 'spec/models/list_spec.rb'
    - 'spec/models/login_activity_spec.rb'
    - 'spec/models/mute_spec.rb'
    - 'spec/models/preview_card_spec.rb'
    - 'spec/models/preview_card_trend_spec.rb'
    - 'spec/models/relay_spec.rb'
    - 'spec/models/scheduled_status_spec.rb'
    - 'spec/models/status_stat_spec.rb'
    - 'spec/models/status_trend_spec.rb'
    - 'spec/models/system_key_spec.rb'
    - 'spec/models/tag_follow_spec.rb'
    - 'spec/models/unavailable_domain_spec.rb'
    - 'spec/models/user_invite_request_spec.rb'
    - 'spec/models/web/setting_spec.rb'
    - 'spec/services/unmute_service_spec.rb'

# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
  Max: 22


@@ 354,43 317,6 @@ Rails/ApplicationController:
  Exclude:
    - 'app/controllers/health_controller.rb'

# Configuration parameters: Database, Include.
# SupportedDatabases: mysql, postgresql
# Include: db/**/*.rb
Rails/BulkChangeTable:
  Exclude:
    - 'db/migrate/20160222143943_add_profile_fields_to_accounts.rb'
    - 'db/migrate/20160223162837_add_metadata_to_statuses.rb'
    - 'db/migrate/20160305115639_add_devise_to_users.rb'
    - 'db/migrate/20160314164231_add_owner_to_application.rb'
    - 'db/migrate/20160926213048_remove_owner_from_application.rb'
    - 'db/migrate/20161003142332_add_confirmable_to_users.rb'
    - 'db/migrate/20170112154826_migrate_settings.rb'
    - 'db/migrate/20170127165745_add_devise_two_factor_to_users.rb'
    - 'db/migrate/20170322143850_change_primary_key_to_bigint_on_statuses.rb'
    - 'db/migrate/20170330021336_add_counter_caches.rb'
    - 'db/migrate/20170425202925_add_oembed_to_preview_cards.rb'
    - 'db/migrate/20170427011934_re_add_owner_to_application.rb'
    - 'db/migrate/20170520145338_change_language_filter_to_opt_out.rb'
    - 'db/migrate/20170624134742_add_description_to_session_activations.rb'
    - 'db/migrate/20170718211102_add_activitypub_to_accounts.rb'
    - 'db/migrate/20171006142024_add_uri_to_custom_emojis.rb'
    - 'db/migrate/20180812123222_change_relays_enabled.rb'
    - 'db/migrate/20190511134027_add_silenced_at_suspended_at_to_accounts.rb'
    - 'db/migrate/20190805123746_add_capabilities_to_tags.rb'
    - 'db/migrate/20190807135426_add_comments_to_domain_blocks.rb'
    - 'db/migrate/20190815225426_add_last_status_at_to_tags.rb'
    - 'db/migrate/20190901035623_add_max_score_to_tags.rb'
    - 'db/migrate/20200417125749_add_storage_schema_version.rb'
    - 'db/migrate/20200608113046_add_sign_in_token_to_users.rb'
    - 'db/migrate/20211112011713_add_language_to_preview_cards.rb'
    - 'db/migrate/20211231080958_add_category_to_reports.rb'
    - 'db/migrate/20220202200743_add_trendable_to_accounts.rb'
    - 'db/migrate/20220224010024_add_ips_to_email_domain_blocks.rb'
    - 'db/migrate/20220227041951_add_last_used_at_to_oauth_access_tokens.rb'
    - 'db/migrate/20220303000827_add_ordered_media_attachment_ids_to_status_edits.rb'
    - 'db/migrate/20220824164433_add_human_identifier_to_admin_action_logs.rb'

# Configuration parameters: Include.
# Include: db/**/*.rb
Rails/CreateTableWithTimestamps:


@@ 666,7 592,7 @@ Style/FetchEnvVar:
    - 'app/lib/translation_service.rb'
    - 'config/environments/development.rb'
    - 'config/environments/production.rb'
    - 'config/initializers/2_whitelist_mode.rb'
    - 'config/initializers/2_limited_federation_mode.rb'
    - 'config/initializers/blacklists.rb'
    - 'config/initializers/cache_buster.rb'
    - 'config/initializers/content_security_policy.rb'


@@ 929,9 855,3 @@ Style/WordArray:
    - 'config/initializers/cors.rb'
    - 'spec/controllers/settings/imports_controller_spec.rb'
    - 'spec/models/form/import_spec.rb'

# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
Layout/LineLength:
  Max: 701

M CHANGELOG.md => CHANGELOG.md +8 -0
@@ 2,6 2,14 @@

All notable changes to this project will be documented in this file.

## [4.1.6] - 2023-07-31

### Fixed

- Fix memory leak in streaming server ([ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26228))
- Fix wrong filters sometimes applying in streaming ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26159), [ThisIsMissEm](https://github.com/mastodon/mastodon/pull/26213), [renchap](https://github.com/mastodon/mastodon/pull/26233))
- Fix incorrect connect timeout in outgoing requests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/26116))

## [4.1.5] - 2023-07-21

### Added

M Gemfile => Gemfile +13 -3
@@ 99,9 99,6 @@ gem 'rdf-normalize', '~> 0.5'
gem 'private_address_check', '~> 0.5'

group :test do
  # RSpec runner for rails
  gem 'rspec-rails', '~> 6.0'

  # Used to split testing into chunks in CI
  gem 'rspec_chunked', '~> 0.6'



@@ 113,6 110,10 @@ group :test do

  # Browser integration testing
  gem 'capybara', '~> 3.39'
  gem 'selenium-webdriver'

  # Used to reset the database between system tests
  gem 'database_cleaner-active_record'

  # Used to mock environment variables
  gem 'climate_control', '~> 0.2'


@@ 173,10 174,19 @@ group :development do

  # Validate missing i18n keys
  gem 'i18n-tasks', '~> 1.0', require: false
end

group :development, :test do
  # Profiling tools
  gem 'memory_profiler', require: false
  gem 'ruby-prof', require: false
  gem 'stackprof', require: false
  gem 'test-prof'
end

group :development, :test do
  # RSpec runner for rails
  gem 'rspec-rails', '~> 6.0'
end

group :production do

M Gemfile.lock => Gemfile.lock +15 -0
@@ 199,6 199,10 @@ GEM
    crass (1.0.6)
    css_parser (1.14.0)
      addressable
    database_cleaner-active_record (2.1.0)
      activerecord (>= 5.a)
      database_cleaner-core (~> 2.0.0)
    database_cleaner-core (2.0.1)
    date (3.3.3)
    debug_inspector (1.1.0)
    devise (4.9.2)


@@ 640,6 644,7 @@ GEM
      rubocop (~> 1.33)
      rubocop-capybara (~> 2.17)
      rubocop-factory_bot (~> 2.22)
    ruby-prof (1.6.3)
    ruby-progressbar (1.13.0)
    ruby-saml (1.15.0)
      nokogiri (>= 1.13.10)


@@ 656,6 661,10 @@ GEM
    scenic (1.7.0)
      activerecord (>= 4.0.0)
      railties (>= 4.0.0)
    selenium-webdriver (4.9.1)
      rexml (~> 3.2, >= 3.2.5)
      rubyzip (>= 1.2.2, < 3.0)
      websocket (~> 1.0)
    semantic_range (3.0.0)
    sidekiq (6.5.9)
      connection_pool (>= 2.2.5, < 3)


@@ 710,6 719,7 @@ GEM
      unicode-display_width (>= 1.1.1, < 3)
    terrapin (0.6.0)
      climate_control (>= 0.0.3, < 1.0)
    test-prof (1.2.1)
    thor (1.2.2)
    tilt (2.2.0)
    timeout (0.4.0)


@@ 768,6 778,7 @@ GEM
      rack-proxy (>= 0.6.1)
      railties (>= 5.2)
      semantic_range (>= 2.3.0)
    websocket (1.2.9)
    websocket-driver (0.7.5)
      websocket-extensions (>= 0.1.0)
    websocket-extensions (0.1.5)


@@ 804,6 815,7 @@ DEPENDENCIES
  color_diff (~> 0.1)
  concurrent-ruby
  connection_pool
  database_cleaner-active_record
  devise (~> 4.9)
  devise-two-factor (~> 4.1)
  devise_pam_authenticatable2 (~> 9.2)


@@ 881,10 893,12 @@ DEPENDENCIES
  rubocop-performance
  rubocop-rails
  rubocop-rspec
  ruby-prof
  ruby-progressbar (~> 1.13)
  rubyzip (~> 2.3)
  sanitize (~> 6.0)
  scenic (~> 1.7)
  selenium-webdriver
  sidekiq (~> 6.5)
  sidekiq-bulk (~> 0.2.0)
  sidekiq-scheduler (~> 5.0)


@@ 897,6 911,7 @@ DEPENDENCIES
  stackprof
  stoplight (~> 3.0.1)
  strong_migrations (~> 0.8)
  test-prof
  thor (~> 1.2)
  tty-prompt (~> 0.23)
  twitter-text (~> 3.1.0)

M app/controllers/accounts_controller.rb => app/controllers/accounts_controller.rb +1 -1
@@ 12,7 12,7 @@ class AccountsController < ApplicationController
  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }

  skip_around_action :set_locale, if: -> { [:json, :rss].include?(request.format&.to_sym) }
  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  def show
    respond_to do |format|

M app/controllers/admin/instances_controller.rb => app/controllers/admin/instances_controller.rb +1 -1
@@ 65,7 65,7 @@ module Admin
    end

    def filtered_instances
      InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
      InstanceFilter.new(limited_federation_mode? ? { allowed: true } : filter_params).results
    end

    def filter_params

M app/controllers/api/base_controller.rb => app/controllers/api/base_controller.rb +2 -2
@@ 8,7 8,7 @@ class Api::BaseController < ApplicationController
  include AccessTokenTrackingConcern
  include ApiCachingConcern

  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
  before_action :require_not_suspended!


@@ 150,7 150,7 @@ class Api::BaseController < ApplicationController
  end

  def disallow_unauthenticated_api_access?
    ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.whitelist_mode
    ENV['DISALLOW_UNAUTHENTICATED_API_ACCESS'] == 'true' || Rails.configuration.x.limited_federation_mode
  end

  private

M app/controllers/api/v1/instances/activity_controller.rb => app/controllers/api/v1/instances/activity_controller.rb +2 -2
@@ 3,7 3,7 @@
class Api::V1::Instances::ActivityController < Api::BaseController
  before_action :require_enabled_api!

  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?

  vary_by ''



@@ 33,6 33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
  end

  def require_enabled_api!
    head 404 unless Setting.activity_api_enabled && !whitelist_mode?
    head 404 unless Setting.activity_api_enabled && !limited_federation_mode?
  end
end

M app/controllers/api/v1/instances/domain_blocks_controller.rb => app/controllers/api/v1/instances/domain_blocks_controller.rb +1 -1
@@ 1,7 1,7 @@
# frozen_string_literal: true

class Api::V1::Instances::DomainBlocksController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?

  before_action :require_enabled_api!
  before_action :set_domain_blocks

M app/controllers/api/v1/instances/extended_descriptions_controller.rb => app/controllers/api/v1/instances/extended_descriptions_controller.rb +2 -2
@@ 1,7 1,7 @@
# frozen_string_literal: true

class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
  skip_around_action :set_locale

  before_action :set_extended_description


@@ 10,7 10,7 @@ class Api::V1::Instances::ExtendedDescriptionsController < Api::BaseController

  # Override `current_user` to avoid reading session cookies unless in whitelist mode
  def current_user
    super if whitelist_mode?
    super if limited_federation_mode?
  end

  def show

M app/controllers/api/v1/instances/peers_controller.rb => app/controllers/api/v1/instances/peers_controller.rb +3 -3
@@ 3,14 3,14 @@
class Api::V1::Instances::PeersController < Api::BaseController
  before_action :require_enabled_api!

  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
  skip_around_action :set_locale

  vary_by ''

  # Override `current_user` to avoid reading session cookies unless in whitelist mode
  def current_user
    super if whitelist_mode?
    super if limited_federation_mode?
  end

  def index


@@ 21,6 21,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
  private

  def require_enabled_api!
    head 404 unless Setting.peers_api_enabled && !whitelist_mode?
    head 404 unless Setting.peers_api_enabled && !limited_federation_mode?
  end
end

M app/controllers/api/v1/instances/privacy_policies_controller.rb => app/controllers/api/v1/instances/privacy_policies_controller.rb +1 -1
@@ 1,7 1,7 @@
# frozen_string_literal: true

class Api::V1::Instances::PrivacyPoliciesController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?

  before_action :set_privacy_policy


M app/controllers/api/v1/instances/rules_controller.rb => app/controllers/api/v1/instances/rules_controller.rb +2 -2
@@ 1,7 1,7 @@
# frozen_string_literal: true

class Api::V1::Instances::RulesController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
  skip_around_action :set_locale

  before_action :set_rules


@@ 10,7 10,7 @@ class Api::V1::Instances::RulesController < Api::BaseController

  # Override `current_user` to avoid reading session cookies unless in whitelist mode
  def current_user
    super if whitelist_mode?
    super if limited_federation_mode?
  end

  def index

M app/controllers/api/v1/instances/translation_languages_controller.rb => app/controllers/api/v1/instances/translation_languages_controller.rb +1 -1
@@ 1,7 1,7 @@
# frozen_string_literal: true

class Api::V1::Instances::TranslationLanguagesController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?

  before_action :set_languages


M app/controllers/api/v1/instances_controller.rb => app/controllers/api/v1/instances_controller.rb +2 -2
@@ 1,14 1,14 @@
# frozen_string_literal: true

class Api::V1::InstancesController < Api::BaseController
  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
  skip_around_action :set_locale

  vary_by ''

  # Override `current_user` to avoid reading session cookies unless in whitelist mode
  def current_user
    super if whitelist_mode?
    super if limited_federation_mode?
  end

  def show

M app/controllers/api/v1/peers/search_controller.rb => app/controllers/api/v1/peers/search_controller.rb +3 -3
@@ 4,7 4,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
  before_action :require_enabled_api!
  before_action :set_domains

  skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
  skip_before_action :require_authenticated_user!, unless: :limited_federation_mode?
  skip_around_action :set_locale

  vary_by ''


@@ 17,7 17,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
  private

  def require_enabled_api!
    head 404 unless Setting.peers_api_enabled && !whitelist_mode?
    head 404 unless Setting.peers_api_enabled && !limited_federation_mode?
  end

  def set_domains


@@ 27,7 27,7 @@ class Api::V1::Peers::SearchController < Api::BaseController
      @domains = InstancesIndex.query(function_score: {
        query: {
          prefix: {
            domain: params[:q],
            domain: TagManager.instance.normalize_domain(params[:q].strip),
          },
        },


M app/controllers/application_controller.rb => app/controllers/application_controller.rb +2 -2
@@ 21,7 21,7 @@ class ApplicationController < ActionController::Base
  helper_method :use_seamless_external_login?
  helper_method :omniauth_only?
  helper_method :sso_account_settings
  helper_method :whitelist_mode?
  helper_method :limited_federation_mode?
  helper_method :body_class_string
  helper_method :skip_csrf_meta_tags?



@@ 54,7 54,7 @@ class ApplicationController < ActionController::Base
  private

  def authorized_fetch_mode?
    ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode
    ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode
  end

  def public_fetch_mode?

M app/controllers/concerns/account_owned_concern.rb => app/controllers/concerns/account_owned_concern.rb +1 -1
@@ 4,7 4,7 @@ module AccountOwnedConcern
  extend ActiveSupport::Concern

  included do
    before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json }
    before_action :authenticate_user!, if: -> { limited_federation_mode? && request.format != :json }
    before_action :set_account, if: :account_required?
    before_action :check_account_approval, if: :account_required?
    before_action :check_account_suspension, if: :account_required?

M app/controllers/concerns/api_caching_concern.rb => app/controllers/concerns/api_caching_concern.rb +1 -1
@@ 8,6 8,6 @@ module ApiCachingConcern
  end

  def cache_even_if_authenticated!
    expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless whitelist_mode?
    expires_in(5.minutes, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.day) unless limited_federation_mode?
  end
end

M app/controllers/follower_accounts_controller.rb => app/controllers/follower_accounts_controller.rb +1 -1
@@ 10,7 10,7 @@ class FollowerAccountsController < ApplicationController
  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }

  skip_around_action :set_locale, if: -> { request.format == :json }
  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  def index
    respond_to do |format|

M app/controllers/following_accounts_controller.rb => app/controllers/following_accounts_controller.rb +1 -1
@@ 10,7 10,7 @@ class FollowingAccountsController < ApplicationController
  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }

  skip_around_action :set_locale, if: -> { request.format == :json }
  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  def index
    respond_to do |format|

M app/controllers/mail_subscriptions_controller.rb => app/controllers/mail_subscriptions_controller.rb +4 -1
@@ 9,6 9,8 @@ class MailSubscriptionsController < ApplicationController
  before_action :set_user
  before_action :set_type

  protect_from_forgery with: :null_session

  def show; end

  def create


@@ 20,6 22,7 @@ class MailSubscriptionsController < ApplicationController

  def set_user
    @user = GlobalID::Locator.locate_signed(params[:token], for: 'unsubscribe')
    not_found unless @user
  end

  def set_body_classes


@@ 35,7 38,7 @@ class MailSubscriptionsController < ApplicationController
    when 'follow', 'reblog', 'favourite', 'mention', 'follow_request'
      "notification_emails.#{params[:type]}"
    else
      raise ArgumentError
      not_found
    end
  end
end

M app/controllers/media_controller.rb => app/controllers/media_controller.rb +2 -2
@@ 3,9 3,9 @@
class MediaController < ApplicationController
  include Authorization

  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  before_action :authenticate_user!, if: :whitelist_mode?
  before_action :authenticate_user!, if: :limited_federation_mode?
  before_action :set_media_attachment
  before_action :verify_permitted_status!
  before_action :check_playable, only: :player

M app/controllers/media_proxy_controller.rb => app/controllers/media_proxy_controller.rb +1 -1
@@ 8,7 8,7 @@ class MediaProxyController < ApplicationController

  skip_before_action :require_functional!

  before_action :authenticate_user!, if: :whitelist_mode?
  before_action :authenticate_user!, if: :limited_federation_mode?

  rescue_from ActiveRecord::RecordInvalid, with: :not_found
  rescue_from Mastodon::UnexpectedResponseError, with: :not_found

M app/controllers/statuses_controller.rb => app/controllers/statuses_controller.rb +1 -1
@@ 17,7 17,7 @@ class StatusesController < ApplicationController
  after_action :set_link_headers

  skip_around_action :set_locale, if: -> { request.format == :json }
  skip_before_action :require_functional!, only: [:show, :embed], unless: :whitelist_mode?
  skip_before_action :require_functional!, only: [:show, :embed], unless: :limited_federation_mode?

  content_security_policy only: :embed do |policy|
    policy.frame_ancestors(false)

M app/controllers/tags_controller.rb => app/controllers/tags_controller.rb +2 -2
@@ 10,13 10,13 @@ class TagsController < ApplicationController
  vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }

  before_action :require_account_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
  before_action :authenticate_user!, if: :whitelist_mode?
  before_action :authenticate_user!, if: :limited_federation_mode?
  before_action :set_local
  before_action :set_tag
  before_action :set_statuses, if: -> { request.format == :rss }
  before_action :set_instance_presenter

  skip_before_action :require_functional!, unless: :whitelist_mode?
  skip_before_action :require_functional!, unless: :limited_federation_mode?

  def show
    respond_to do |format|

M app/helpers/domain_control_helper.rb => app/helpers/domain_control_helper.rb +3 -3
@@ 10,14 10,14 @@ module DomainControlHelper
               uri_or_domain
             end

    if whitelist_mode?
    if limited_federation_mode?
      !DomainAllow.allowed?(domain)
    else
      DomainBlock.blocked?(domain)
    end
  end

  def whitelist_mode?
    Rails.configuration.x.whitelist_mode
  def limited_federation_mode?
    Rails.configuration.x.limited_federation_mode
  end
end

M app/helpers/languages_helper.rb => app/helpers/languages_helper.rb +11 -4
@@ 204,7 204,17 @@ module LanguagesHelper
    zgh: ['Standard Moroccan Tamazight', 'ⵜⴰⵎⴰⵣⵉⵖⵜ'].freeze,
  }.freeze

  SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_3).freeze
  # e.g. For Chinese, which is not a language,
  # but a language family in spite of sharing the main locale code
  # We need to be able to filter these
  ISO_639_1_REGIONAL = {
    'zh-CN': ['Chinese (China)', '简体中文'].freeze,
    'zh-HK': ['Chinese (Hong Kong)', '繁體中文(香港)'].freeze,
    'zh-TW': ['Chinese (Taiwan)', '繁體中文(臺灣)'].freeze,
    'zh-YUE': ['Cantonese', '廣東話'].freeze,
  }.freeze

  SUPPORTED_LOCALES = {}.merge(ISO_639_1).merge(ISO_639_1_REGIONAL).merge(ISO_639_3).freeze

  # For ISO-639-1 and ISO-639-3 language codes, we have their official
  # names, but for some translations, we need the names of the


@@ 217,9 227,6 @@ module LanguagesHelper
    'pt-BR': 'Português (Brasil)',
    'pt-PT': 'Português (Portugal)',
    'sr-Latn': 'Srpski (latinica)',
    'zh-CN': '简体中文',
    'zh-HK': '繁體中文(香港)',
    'zh-TW': '繁體中文(臺灣)',
  }.freeze

  def native_locale_name(locale)

M app/javascript/flavours/glitch/features/interaction_modal/index.jsx => app/javascript/flavours/glitch/features/interaction_modal/index.jsx +3 -0
@@ 250,6 250,9 @@ class LoginForm extends React.PureComponent {
            onFocus={this.handleFocus}
            onBlur={this.handleBlur}
            onKeyDown={this.handleKeyDown}
            autocomplete='off'
            autocapitalize='off'
            spellcheck='false'
          />

          <Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>

A app/javascript/mastodon/components/badge.jsx => app/javascript/mastodon/components/badge.jsx +34 -0
@@ 0,0 1,34 @@
import PropTypes from 'prop-types';

import { FormattedMessage } from 'react-intl';

import { ReactComponent as GroupsIcon } from '@material-design-icons/svg/outlined/group.svg';
import { ReactComponent as PersonIcon } from '@material-design-icons/svg/outlined/person.svg';
import { ReactComponent as SmartToyIcon } from '@material-design-icons/svg/outlined/smart_toy.svg';


export const Badge = ({ icon, label, domain }) => (
  <div className='account-role'>
    {icon}
    {label}
    {domain && <span className='account-role__domain'>{domain}</span>}
  </div>
);

Badge.propTypes = {
  icon: PropTypes.node,
  label: PropTypes.node,
  domain: PropTypes.node,
};

Badge.defaultProps = {
  icon: <PersonIcon />,
};

export const GroupBadge = () => (
  <Badge icon={<GroupsIcon />} label={<FormattedMessage id='account.badges.group' defaultMessage='Group' />} />
);

export const AutomatedBadge = () => (
  <Badge icon={<SmartToyIcon />} label={<FormattedMessage id='account.badges.bot' defaultMessage='Automated' />} />
);
\ No newline at end of file

M app/javascript/mastodon/features/account/components/header.jsx => app/javascript/mastodon/features/account/components/header.jsx +4 -18
@@ 10,6 10,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';

import { Avatar } from 'mastodon/components/avatar';
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge';
import Button from 'mastodon/components/button';
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters';
import { Icon }  from 'mastodon/components/icon';


@@ 373,28 374,13 @@ class Header extends ImmutablePureComponent {
    const badges = [];

    if (account.get('bot')) {
      badges.push(
        <div key='bot-badge' className='account-role bot'>
          <Icon id='cogs' /> { ' ' }
          <FormattedMessage id='account.badges.bot' defaultMessage='Automated' />
        </div>
      );
      badges.push(<AutomatedBadge key='bot-badge' />);
    } else if (account.get('group')) {
      badges.push(
        <div key='group-badge' className='account-role group'>
          <Icon id='users' /> { ' ' }
          <FormattedMessage id='account.badges.group' defaultMessage='Group' />
        </div>
      );
      badges.push(<GroupBadge key='group-badge' />);
    }

    account.get('roles', []).forEach((role) => {
      badges.push(
        <div key={`role-badge-${role.get('id')}`} className={`account-role user-role-${account.getIn(['roles', 0, 'id'])}`}>
          <Icon id='circle' /> { ' ' }
          <span>{role.get('name')} ({domain})</span>
        </div>
      );
      badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} />);
    });

    return (

M app/javascript/mastodon/features/interaction_modal/index.jsx => app/javascript/mastodon/features/interaction_modal/index.jsx +3 -0
@@ 250,6 250,9 @@ class LoginForm extends React.PureComponent {
            onFocus={this.handleFocus}
            onBlur={this.handleBlur}
            onKeyDown={this.handleKeyDown}
            autocomplete='off'
            autocapitalize='off'
            spellcheck='false'
          />

          <Button onClick={this.handleSubmit} disabled={isSubmitting}><FormattedMessage id='interaction_modal.login.action' defaultMessage='Take me home' /></Button>

M app/javascript/mastodon/features/public_timeline/index.jsx => app/javascript/mastodon/features/public_timeline/index.jsx +1 -1
@@ 29,7 29,7 @@ const mapStateToProps = (state, { columnId }) => {
  const index = columns.findIndex(c => c.get('uuid') === uuid);
  const onlyMedia = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'public', 'other', 'onlyMedia']);
  const onlyRemote = (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyRemote']) : state.getIn(['settings', 'public', 'other', 'onlyRemote']);
  const timelineState = state.getIn(['timelines', `public${onlyMedia ? ':media' : ''}`]);
  const timelineState = state.getIn(['timelines', `public${onlyRemote ? ':remote' : ''}${onlyMedia ? ':media' : ''}`]);

  return {
    hasUnread: !!timelineState && timelineState.get('unread') > 0,

M app/javascript/mastodon/features/status/components/card.jsx => app/javascript/mastodon/features/status/components/card.jsx +29 -53
@@ 5,7 5,7 @@ import { PureComponent } from 'react';

import { FormattedMessage } from 'react-intl';

import classnames from 'classnames';
import classNames from 'classnames';

import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';


@@ 71,6 71,7 @@ export default class Card extends PureComponent {
    if (!Immutable.is(this.props.card, nextProps.card)) {
      this.setState({ embedded: false, previewLoaded: false });
    }

    if (this.props.sensitive !== nextProps.sensitive) {
      this.setState({ revealed: !nextProps.sensitive });
    }


@@ 84,35 85,8 @@ export default class Card extends PureComponent {
    window.removeEventListener('resize', this.handleResize);
  }

  handlePhotoClick = () => {
    const { card, onOpenMedia } = this.props;

    onOpenMedia(
      Immutable.fromJS([
        {
          type: 'image',
          url: card.get('embed_url'),
          description: card.get('title'),
          meta: {
            original: {
              width: card.get('width'),
              height: card.get('height'),
            },
          },
        },
      ]),
      0,
    );
  };

  handleEmbedClick = () => {
    const { card } = this.props;

    if (card.get('type') === 'photo') {
      this.handlePhotoClick();
    } else {
      this.setState({ embedded: true });
    }
    this.setState({ embedded: true });
  };

  setRef = c => {


@@ 130,15 104,15 @@ export default class Card extends PureComponent {
  };

  renderVideo () {
    const { card }  = this.props;
    const content   = { __html: addAutoPlay(card.get('html')) };
    const { card } = this.props;
    const content = { __html: addAutoPlay(card.get('html')) };

    return (
      <div
        ref={this.setRef}
        className='status-card__image status-card-video'
        dangerouslySetInnerHTML={content}
        style={{ aspectRatio: `${card.get('width')} / ${card.get('height')}` }}
        style={{ aspectRatio: '16 / 9' }}
      />
    );
  }


@@ 152,30 126,40 @@ export default class Card extends PureComponent {
    }

    const provider    = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name');
    const interactive = card.get('type') !== 'link';
    const interactive = card.get('type') === 'video';
    const language    = card.get('language') || '';
    const largeImage  = (card.get('image')?.length > 0 && card.get('width') > card.get('height')) || interactive;

    const description = (
      <div className='status-card__content'>
        <span className='status-card__host'>
          <span lang={language}>{provider}</span>
          {card.get('published_at') && <> · <RelativeTimestamp timestamp={card.get('published_at')} /></>}
         </span>
        </span>

        <strong className='status-card__title' title={card.get('title')} lang={language}>{card.get('title')}</strong>
        {card.get('author_name').length > 0 && <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span>}

        {card.get('author_name').length > 0 ? <span className='status-card__author'><FormattedMessage id='link_preview.author' defaultMessage='By {name}' values={{ name: <strong>{card.get('author_name')}</strong> }} /></span> : <span className='status-card__description'>{card.get('description')}</span>}
      </div>
    );

    const thumbnailStyle = {
      visibility: revealed ? null : 'hidden',
      aspectRatio: `${card.get('width')} / ${card.get('height')}`
    };

    if (largeImage && card.get('type') === 'video') {
      thumbnailStyle.aspectRatio = `16 / 9`;
    } else if (largeImage) {
      thumbnailStyle.aspectRatio = '1.91 / 1';
    } else {
      thumbnailStyle.aspectRatio = 1;
    }

    let embed;

    let canvas = (
      <Blurhash
        className={classnames('status-card__image-preview', {
        className={classNames('status-card__image-preview', {
          'status-card__image-preview--hidden': revealed && this.state.previewLoaded,
        })}
        hash={card.get('blurhash')}


@@ 195,7 179,7 @@ export default class Card extends PureComponent {
    );

    spoilerButton = (
      <div className={classnames('spoiler-button', { 'spoiler-button--minified': revealed })}>
      <div className={classNames('spoiler-button', { 'spoiler-button--minified': revealed })}>
        {spoilerButton}
      </div>
    );


@@ 204,33 188,25 @@ export default class Card extends PureComponent {
      if (embedded) {
        embed = this.renderVideo();
      } else {
        let iconVariant = 'play';

        if (card.get('type') === 'photo') {
          iconVariant = 'search-plus';
        }

        embed = (
          <div className='status-card__image'>
            {canvas}
            {thumbnail}

            {revealed && (
              <div className='status-card__actions'>
            {revealed ? (
              <div className='status-card__actions' onClick={this.handleEmbedClick} role='none'>
                <div>
                  <button type='button' onClick={this.handleEmbedClick}><Icon id={iconVariant} /></button>
                  <button type='button' onClick={this.handleEmbedClick}><Icon id='play' /></button>
                  <a href={card.get('url')} target='_blank' rel='noopener noreferrer'><Icon id='external-link' /></a>
                </div>
              </div>
            )}

            {!revealed && spoilerButton}
            ) : spoilerButton}
          </div>
        );
      }

      return (
        <div className='status-card' ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
        <div className={classNames('status-card', { expanded: largeImage })} ref={this.setRef} onClick={revealed ? null : this.handleReveal} role={revealed ? 'button' : null}>
          {embed}
          <a href={card.get('url')} target='_blank' rel='noopener noreferrer'>{description}</a>
        </div>


@@ 244,14 220,14 @@ export default class Card extends PureComponent {
      );
    } else {
      embed = (
        <div className='status-card__image' style={{ aspectRatio: '1.9 / 1' }}>
        <div className='status-card__image'>
          <Icon id='file-text' />
        </div>
      );
    }

    return (
      <a href={card.get('url')} className='status-card' target='_blank' rel='noopener noreferrer' ref={this.setRef}>
      <a href={card.get('url')} className={classNames('status-card', { expanded: largeImage })} target='_blank' rel='noopener noreferrer' ref={this.setRef}>
        {embed}
        {description}
      </a>

M app/javascript/styles/mastodon/accounts.scss => app/javascript/styles/mastodon/accounts.scss +22 -3
@@ 187,7 187,6 @@
  }
}

.account-role,
.information-badge,
.simple_form .recommended,
.simple_form .not_recommended {


@@ 212,10 211,30 @@
}

.account-role {
  display: inline-flex;
  padding: 4px;
  padding-inline-end: 8px;
  border: 1px solid $highlight-text-color;
  color: $highlight-text-color;
  font-weight: 500;
  font-size: 12px;
  letter-spacing: 0.5px;
  line-height: 16px;
  gap: 4px;
  border-radius: 6px;
  align-items: center;

  svg {
    width: auto;
    height: 15px;
    opacity: 0.85;
    fill: currentColor;
  }

  .fa {
    color: var(--user-role-accent, $highlight-text-color);
  &__domain {
    font-weight: 400;
    opacity: 0.75;
    letter-spacing: 0;
  }
}


M app/javascript/styles/mastodon/basics.scss => app/javascript/styles/mastodon/basics.scss +1 -1
@@ 164,7 164,7 @@ body {
a {
  &:focus {
    border-radius: 4px;
    outline: $ui-button-icon-focus-outline;
    outline: $ui-button-focus-outline;
  }

  &:focus:not(:focus-visible) {

M app/javascript/styles/mastodon/components.scss => app/javascript/styles/mastodon/components.scss +81 -12
@@ 3283,6 3283,8 @@ $ui-header-height: 55px;
  text-decoration: none;
  overflow: hidden;
  white-space: nowrap;
  border: 0;
  border-left: 4px solid transparent;

  &:hover,
  &:focus,


@@ 3294,6 3296,11 @@ $ui-header-height: 55px;
    outline: 0;
  }

  &:focus-visible {
    border-color: $ui-button-focus-outline-color;
    border-radius: 0;
  }

  &--transparent {
    background: transparent;
    color: $ui-secondary-color;


@@ 3510,13 3517,16 @@ button.icon-button.active i.fa-retweet {
}

.status-card {
  display: block;
  display: flex;
  align-items: center;
  position: relative;
  font-size: 14px;
  color: $darker-text-color;
  margin-top: 14px;
  text-decoration: none;
  overflow: hidden;
  border: 1px solid lighten($ui-base-color, 8%);
  border-radius: 8px;

  &__actions {
    bottom: 0;


@@ 3527,11 3537,13 @@ button.icon-button.active i.fa-retweet {
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;

    & > div {
      background: rgba($base-shadow-color, 0.6);
      border-radius: 8px;
      padding: 12px 9px;
      backdrop-filter: blur(10px) saturate(180%) contrast(75%) brightness(70%);
      flex: 0 0 auto;
      display: flex;
      justify-content: center;


@@ 3572,7 3584,8 @@ a.status-card {
  &:active {
    .status-card__title,
    .status-card__host,
    .status-card__author {
    .status-card__author,
    .status-card__description {
      color: $highlight-text-color;
    }
  }


@@ 3587,7 3600,8 @@ a.status-card {
  &:active {
    .status-card__title,
    .status-card__host,
    .status-card__author {
    .status-card__author,
    .status-card__description {
      color: $highlight-text-color;
    }
  }


@@ 3620,19 3634,32 @@ a.status-card {
  line-height: 24px;
  color: $primary-text-color;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.status-card.expanded .status-card__title {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}

.status-card__content {
  flex: 1 1 auto;
  overflow: hidden;
  padding: 15px 0;
  padding-bottom: 0;
  padding: 15px;
  box-sizing: border-box;
  max-width: 100%;
}

.status-card__host {
  display: block;
  font-size: 14px;
  margin-bottom: 8px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.status-card__author {


@@ 3640,17 3667,30 @@ a.status-card {
  margin-top: 8px;
  font-size: 14px;
  color: $primary-text-color;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;

  strong {
    font-weight: 500;
  }
}

.status-card__description {
  display: block;
  margin-top: 8px;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.status-card__image {
  width: 100%;
  flex: 0 0 auto;
  width: 120px;
  aspect-ratio: 1;
  background: lighten($ui-base-color, 8%);
  position: relative;
  border-radius: 8px;

  & > .fa {
    font-size: 21px;


@@ 3663,7 3703,6 @@ a.status-card {
}

.status-card__image-image {
  border-radius: 8px;
  display: block;
  margin: 0;
  width: 100%;


@@ 3674,7 3713,6 @@ a.status-card {
}

.status-card__image-preview {
  border-radius: 8px;
  display: block;
  margin: 0;
  width: 100%;


@@ 3691,6 3729,37 @@ a.status-card {
  }
}

.status-card.expanded {
  flex-direction: column;
  align-items: flex-start;
}

.status-card.expanded .status-card__image {
  width: 100%;
  aspect-ratio: auto;
}

.status-card__image,
.status-card__image-image,
.status-card__image-preview {
  border-start-start-radius: 8px;
  border-start-end-radius: 0;
  border-end-end-radius: 0;
  border-end-start-radius: 8px;
}

.status-card.expanded .status-card__image,
.status-card.expanded .status-card__image-image,
.status-card.expanded .status-card__image-preview {
  border-start-end-radius: 8px;
  border-end-end-radius: 0;
  border-end-start-radius: 0;
}

.status-card.expanded > a {
  width: 100%;
}

.load-more {
  display: block;
  color: $dark-text-color;


@@ 3896,7 3965,7 @@ a.status-card {
  }

  &:focus-visible {
    outline: $ui-button-icon-focus-outline;
    outline: $ui-button-focus-outline;
  }

  &.active {


@@ 4902,7 4971,7 @@ a.status-card {
    width: 100%;
    background: $ui-base-color;
    border-radius: 0 0 4px 4px;
    box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4);
    box-shadow: var(--dropdown-shadow);
    z-index: 99;
    font-size: 13px;
    padding: 15px 5px;


@@ 8218,7 8287,7 @@ noscript {
    flex: 0 0 auto;
    position: relative;
    width: 120px;
    height: 120px;
    aspect-ratio: 1;

    .skeleton {
      width: 100%;

M app/javascript/styles/mastodon/variables.scss => app/javascript/styles/mastodon/variables.scss +3 -1
@@ 43,6 43,8 @@ $ui-highlight-color: $classic-highlight-color !default;
$ui-button-color: $white !default;
$ui-button-background-color: $blurple-500 !default;
$ui-button-focus-background-color: $blurple-600 !default;
$ui-button-focus-outline-color: $blurple-400 !default;
$ui-button-focus-outline: solid 2px $ui-button-focus-outline-color !default;

$ui-button-secondary-color: $grey-100 !default;
$ui-button-secondary-border-color: $grey-100 !default;


@@ 57,7 59,7 @@ $ui-button-tertiary-focus-color: $white !default;
$ui-button-destructive-background-color: $red-500 !default;
$ui-button-destructive-focus-background-color: $red-600 !default;

$ui-button-icon-focus-outline: solid 2px $blurple-400 !default;
$ui-button-icon-focus-outline: $ui-button-focus-outline !default;
$ui-button-icon-hover-background-color: rgba(140, 141, 255, 40%) !default;

// Variables for texts

M app/lib/importer/base_importer.rb => app/lib/importer/base_importer.rb +4 -1
@@ 45,8 45,11 @@ class Importer::BaseImporter
  # Remove documents from the index that no longer exist in the database
  def clean_up!
    index.scroll_batches do |documents|
      primary_key = index.adapter.target.primary_key
      raise ActiveRecord::UnknownPrimaryKey, index.adapter.target if primary_key.nil?

      ids           = documents.pluck('_id')
      existence_map = index.adapter.target.where(id: ids).pluck(:id).each_with_object({}) { |id, map| map[id.to_s] = true }
      existence_map = index.adapter.target.where(primary_key => ids).pluck(primary_key).each_with_object({}) { |id, map| map[id.to_s] = true }
      tmp           = ids.reject { |id| existence_map[id] }

      next if tmp.empty?

M app/lib/request.rb => app/lib/request.rb +21 -2
@@ 68,13 68,26 @@ class Request
  # about 15s in total
  TIMEOUT = { connect_timeout: 5, read_timeout: 10, write_timeout: 10, read_deadline: 30 }.freeze

  # Workaround for overly-eager decoding of percent-encoded characters in Addressable::URI#normalized_path
  # https://github.com/sporkmonger/addressable/issues/366
  URI_NORMALIZER = lambda do |uri|
    uri = HTTP::URI.parse(uri)

    HTTP::URI.new(
      scheme: uri.normalized_scheme,
      authority: uri.normalized_authority,
      path: Addressable::URI.normalize_path(encode_non_ascii(uri.path)).presence || '/',
      query: encode_non_ascii(uri.query)
    )
  end

  include RoutingHelper

  def initialize(verb, url, **options)
    raise ArgumentError if url.blank?

    @verb        = verb
    @url         = Addressable::URI.parse(url).normalize
    @url         = URI_NORMALIZER.call(url)
    @http_client = options.delete(:http_client)
    @allow_local = options.delete(:allow_local)
    @options     = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)


@@ 138,8 151,14 @@ class Request
      %w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
    end

    NON_ASCII_PATTERN = /[^\x00-\x7F]+/

    def encode_non_ascii(str)
      str&.gsub(NON_ASCII_PATTERN) { |substr| CGI.escape(substr.encode(Encoding::UTF_8)) }
    end

    def http_client
      HTTP.use(:auto_inflate).follow(max_hops: 3)
      HTTP.use(:auto_inflate).use(normalize_uri: { normalizer: URI_NORMALIZER }).follow(max_hops: 3)
    end
  end


M app/lib/rss/builder.rb => app/lib/rss/builder.rb +9 -1
@@ 14,13 14,14 @@ class RSS::Builder
  end

  def to_xml
    ('<?xml version="1.0" encoding="UTF-8"?>'.dup << Ox.dump(wrap_in_document, effort: :tolerant)).force_encoding('UTF-8')
    Ox.dump(wrap_in_document, effort: :tolerant).force_encoding('UTF-8')
  end

  private

  def wrap_in_document
    Ox::Document.new(version: '1.0').tap do |document|
      document << xml_instruct
      document << Ox::Element.new('rss').tap do |rss|
        rss['version']        = '2.0'
        rss['xmlns:webfeeds'] = 'http://webfeeds.org/rss/1.0'


@@ 30,4 31,11 @@ class RSS::Builder
      end
    end
  end

  def xml_instruct
    Ox::Instruct.new(:xml).tap do |instruct|
      instruct[:version] = '1.0'
      instruct[:encoding] = 'UTF-8'
    end
  end
end

M app/lib/text_formatter.rb => app/lib/text_formatter.rb +1 -1
@@ 75,7 75,7 @@ class TextFormatter
      entity[:indices].first
    end

    result = ''.dup
    result = +''

    last_index = entities.reduce(0) do |index, entity|
      indices = entity[:indices]

M app/mailers/notification_mailer.rb => app/mailers/notification_mailer.rb +8 -0
@@ 8,6 8,7 @@ class NotificationMailer < ApplicationMailer
  before_action :process_params
  before_action :set_status, only: [:mention, :favourite, :reblog]
  before_action :set_account, only: [:follow, :favourite, :reblog, :follow_request]
  after_action :set_list_headers!

  default to: -> { email_address_with_name(@user.email, @me.username) }



@@ 61,6 62,7 @@ class NotificationMailer < ApplicationMailer
    @me = params[:recipient]
    @user = @me.user
    @type = action_name
    @unsubscribe_url = unsubscribe_url(token: @user.to_sgid(for: 'unsubscribe').to_s, type: @type)
  end

  def set_status


@@ 71,6 73,12 @@ class NotificationMailer < ApplicationMailer
    @account = @notification.from_account
  end

  def set_list_headers!
    headers['List-ID'] = "<#{@type}.#{@me.username}.#{Rails.configuration.x.local_domain}>"
    headers['List-Unsubscribe'] = "<#{@unsubscribe_url}>"
    headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click'
  end

  def thread_by_conversation(conversation)
    return if conversation.nil?


M app/models/media_attachment.rb => app/models/media_attachment.rb +1 -1
@@ 57,7 57,7 @@ class MediaAttachment < ApplicationRecord
  ).freeze

  IMAGE_MIME_TYPES             = %w(image/jpeg image/png image/gif image/heic image/heif image/webp image/avif).freeze
  IMAGE_CONVERTIBLE_MIME_TYPES = %w(image/heic image/heif).freeze
  IMAGE_CONVERTIBLE_MIME_TYPES = %w(image/heic image/heif image/avif).freeze
  VIDEO_MIME_TYPES             = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
  VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
  AUDIO_MIME_TYPES             = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/vnd.wave audio/ogg audio/vorbis audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf).freeze

M app/serializers/initial_state_serializer.rb => app/serializers/initial_state_serializer.rb +1 -1
@@ 36,7 36,7 @@ class InitialStateSerializer < ActiveModel::Serializer
      repository: Mastodon::Version.repository,
      source_url: instance_presenter.source_url,
      version: instance_presenter.version,
      limited_federation_mode: Rails.configuration.x.whitelist_mode,
      limited_federation_mode: Rails.configuration.x.limited_federation_mode,
      mascot: instance_presenter.mascot&.file&.url,
      profile_directory: Setting.profile_directory,
      trends_enabled: Setting.trends,

M app/services/concerns/payloadable.rb => app/services/concerns/payloadable.rb +1 -1
@@ 23,6 23,6 @@ module Payloadable
  end

  def signing_enabled?
    ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.whitelist_mode
    ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.limited_federation_mode
  end
end

M app/services/fetch_link_card_service.rb => app/services/fetch_link_card_service.rb +6 -8
@@ 45,20 45,18 @@ class FetchLinkCardService < BaseService
  def html
    return @html if defined?(@html)

    Request.new(:get, @url).add_headers('Accept' => 'text/html', 'User-Agent' => "#{Mastodon::Version.user_agent} Bot").perform do |res|
    @html = Request.new(:get, @url).add_headers('Accept' => 'text/html', 'User-Agent' => "#{Mastodon::Version.user_agent} Bot").perform do |res|
      next unless res.code == 200 && res.mime_type == 'text/html'

      # We follow redirects, and ideally we want to save the preview card for
      # the destination URL and not any link shortener in-between, so here
      # we set the URL to the one of the last response in the redirect chain
      @url  = res.request.uri.to_s
      @card = PreviewCard.find_or_initialize_by(url: @url) if @card.url != @url

      if res.code == 200 && res.mime_type == 'text/html'
        @html_charset = res.charset
        @html = res.body_with_limit
      else
        @html_charset = nil
        @html = nil
      end
      @html_charset = res.charset

      res.body_with_limit
    end
  end


M app/services/unallow_domain_service.rb => app/services/unallow_domain_service.rb +1 -1
@@ 4,7 4,7 @@ class UnallowDomainService < BaseService
  include DomainControlHelper

  def call(domain_allow)
    suspend_accounts!(domain_allow.domain) if whitelist_mode?
    suspend_accounts!(domain_allow.domain) if limited_federation_mode?

    domain_allow.destroy
  end

M app/validators/language_validator.rb => app/validators/language_validator.rb +8 -6
@@ 4,18 4,20 @@ class LanguageValidator < ActiveModel::EachValidator
  include LanguagesHelper

  def validate_each(record, attribute, value)
    record.errors.add(attribute, :invalid) unless valid?(value)
    @value = value

    record.errors.add(attribute, :invalid) unless valid_locale_value?
  end

  private

  def valid?(str)
    if str.nil?
  def valid_locale_value?
    if @value.nil?
      true
    elsif str.is_a?(Array)
      str.all? { |x| valid_locale?(x) }
    elsif @value.is_a?(Array)
      @value.all? { |x| valid_locale?(x) }
    else
      valid_locale?(str)
      valid_locale?(@value)
    end
  end
end

M app/validators/status_length_validator.rb => app/validators/status_length_validator.rb +1 -1
@@ 45,7 45,7 @@ class StatusLengthValidator < ActiveModel::Validator

  def rewrite_entities(str, entities)
    entities.sort_by! { |entity| entity[:indices].first }
    result = ''.dup
    result = +''

    last_index = entities.reduce(0) do |index, entity|
      result << str[index...entity[:indices].first]

M app/validators/url_validator.rb => app/validators/url_validator.rb +19 -4
@@ 1,16 1,31 @@
# frozen_string_literal: true

class URLValidator < ActiveModel::EachValidator
  VALID_SCHEMES = %w(http https).freeze

  def validate_each(record, attribute, value)
    record.errors.add(attribute, :invalid) unless compliant?(value)
    @value = value

    record.errors.add(attribute, :invalid) unless compliant_url?
  end

  private

  def compliant?(url)
    parsed_url = Addressable::URI.parse(url)
    parsed_url && %w(http https).include?(parsed_url.scheme) && parsed_url.host
  def compliant_url?
    parsed_url.present? && valid_url_scheme? && valid_url_host?
  end

  def parsed_url
    Addressable::URI.parse(@value)
  rescue Addressable::URI::InvalidURIError
    false
  end

  def valid_url_scheme?
    VALID_SCHEMES.include?(parsed_url.scheme)
  end

  def valid_url_host?
    parsed_url.host.present?
  end
end

M app/views/admin/instances/index.html.haml => app/views/admin/instances/index.html.haml +3 -3
@@ 2,7 2,7 @@
  = t('admin.instances.title')

- content_for :heading_actions do
  - if whitelist_mode?
  - if limited_federation_mode?
    = link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button', id: 'add-instance-button'
    = link_to t('admin.domain_allows.export'), export_admin_export_domain_allows_path(format: :csv), class: 'button'
    = link_to t('admin.domain_allows.import'), new_admin_export_domain_allow_path, class: 'button'


@@ 17,7 17,7 @@
    %ul
      %li= filter_link_to t('admin.instances.moderation.all'), limited: nil

      - unless whitelist_mode?
      - unless limited_federation_mode?
        %li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'

  .filter-subset


@@ 27,7 27,7 @@
      %li= filter_link_to t('admin.instances.delivery.failing'), availability: 'failing'
      %li= filter_link_to t('admin.instances.delivery.unavailable'), availability: 'unavailable'

- unless whitelist_mode?
- unless limited_federation_mode?
  = form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
    .fields-group
      - InstanceFilter::KEYS.each do |key|

M app/views/admin/instances/show.html.haml => app/views/admin/instances/show.html.haml +1 -1
@@ 33,7 33,7 @@

%h3= t('admin.instances.content_policies.title')

- if whitelist_mode?
- if limited_federation_mode?
  %p= t('admin.instances.content_policies.limited_federation_mode_description_html')

  - if @instance.domain_allow

M app/views/layouts/mailer.html.haml => app/views/layouts/mailer.html.haml +2 -2
@@ 46,9 46,9 @@
                                %p= t 'about.hosted_on', domain: site_hostname
                                %p
                                  = link_to t('application_mailer.notification_preferences'), settings_preferences_notifications_url
                                  - if defined?(@type)
                                  - if defined?(@unsubscribe_url)
                                    ·
                                    = link_to t('application_mailer.unsubscribe'), unsubscribe_url(token: @user.to_sgid(for: 'unsubscribe').to_s, type: @type)
                                    = link_to t('application_mailer.unsubscribe'), @unsubscribe_url
                              %td.column-cell.text-right
                                = link_to root_url do
                                  = image_tag full_pack_url('media/images/mailer/logo.png'), alt: 'Mastodon', height: 24

M app/views/well_known/host_meta/show.xml.ruby => app/views/well_known/host_meta/show.xml.ruby +8 -1
@@ 2,6 2,13 @@

doc = Ox::Document.new(version: '1.0')

ins = Ox::Instruct.new(:xml).tap do |instruct|
  instruct[:version] = '1.0'
  instruct[:encoding] = 'UTF-8'
end

doc << ins

doc << Ox::Element.new('XRD').tap do |xrd|
  xrd['xmlns'] = 'http://docs.oasis-open.org/ns/xri/xrd-1.0'



@@ 11,4 18,4 @@ doc << Ox::Element.new('XRD').tap do |xrd|
  end
end

"<?xml version=\"1.0\" encoding=\"UTF-8\"?>#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8')
Ox.dump(doc, effort: :tolerant).force_encoding('UTF-8')

M config/application.rb => config/application.rb +1 -2
@@ 46,7 46,6 @@ require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_controller/conditional_get_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'


@@ 199,7 198,7 @@ module Mastodon
    # We use our own middleware for this
    config.public_file_server.enabled = false

    config.middleware.use PublicFileServerMiddleware if Rails.env.development? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
    config.middleware.use PublicFileServerMiddleware if Rails.env.development? || Rails.env.test? || ENV['RAILS_SERVE_STATIC_FILES'] == 'true'
    config.middleware.use Rack::Attack
    config.middleware.use Mastodon::RackMiddleware


M config/brakeman.ignore => config/brakeman.ignore +0 -92
@@ 58,75 58,6 @@
      "note": ""
    },
    {
      "warning_type": "Mass Assignment",
      "warning_code": 105,
      "fingerprint": "874be88fedf4c680926845e9a588d3197765a6ccbfdd76466b44cc00151c612e",
      "check_name": "PermitAttributes",
      "message": "Potentially dangerous key allowed for mass assignment",
      "file": "app/controllers/api/v1/admin/reports_controller.rb",
      "line": 88,
      "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
      "code": "params.permit(:resolved, :account_id, :target_account_id)",
      "render_path": null,
      "location": {
        "type": "method",
        "class": "Api::V1::Admin::ReportsController",
        "method": "filter_params"
      },
      "user_input": ":account_id",
      "confidence": "High",
      "cwe_id": [
        915
      ],
      "note": ""
    },
    {
      "warning_type": "Mass Assignment",
      "warning_code": 105,
      "fingerprint": "ab5035dd1a9f8c3a8d92fb2c37e8fe86fede4f87c91b71aa32e89c9eede602fc",
      "check_name": "PermitAttributes",
      "message": "Potentially dangerous key allowed for mass assignment",
      "file": "app/controllers/api/v1/notifications_controller.rb",
      "line": 77,
      "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
      "code": "params.permit(:account_id, :types => ([]), :exclude_types => ([]))",
      "render_path": null,
      "location": {
        "type": "method",
        "class": "Api::V1::NotificationsController",
        "method": "browserable_params"
      },
      "user_input": ":account_id",
      "confidence": "High",
      "cwe_id": [
        915
      ],
      "note": ""
    },
    {
      "warning_type": "Mass Assignment",
      "warning_code": 105,
      "fingerprint": "b0dd0a26d24f5ede9713fe49210e9638be5f5548af9eee0b5a16fe9dbc80ffcd",
      "check_name": "PermitAttributes",
      "message": "Potentially dangerous key allowed for mass assignment",
      "file": "app/controllers/api/v2/search_controller.rb",
      "line": 42,
      "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
      "code": "params.permit(:type, :offset, :min_id, :max_id, :account_id, :following)",
      "render_path": null,
      "location": {
        "type": "method",
        "class": "Api::V2::SearchController",
        "method": "search_params"
      },
      "user_input": ":account_id",
      "confidence": "High",
      "cwe_id": [
        915
      ],
      "note": ""
    },
    {
      "warning_type": "Cross-Site Scripting",
      "warning_code": 4,
      "fingerprint": "cd5cfd7f40037fbfa753e494d7129df16e358bfc43ef0da3febafbf4ee1ed3ac",


@@ 158,29 89,6 @@
        79
      ],
      "note": ""
    },
    {
      "warning_type": "Mass Assignment",
      "warning_code": 105,
      "fingerprint": "d0511f0287aea4ed9511f5a744f880cb15af77a8ec88f81b7365b00b642cf427",
      "check_name": "PermitAttributes",
      "message": "Potentially dangerous key allowed for mass assignment",
      "file": "app/controllers/api/v1/reports_controller.rb",
      "line": 26,
      "link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
      "code": "params.permit(:account_id, :comment, :category, :forward, :forward_to_domains => ([]), :status_ids => ([]), :rule_ids => ([]))",
      "render_path": null,
      "location": {
        "type": "method",
        "class": "Api::V1::ReportsController",
        "method": "report_params"
      },
      "user_input": ":account_id",
      "confidence": "High",
      "cwe_id": [
        915
      ],
      "note": ""
    }
  ],
  "updated": "2023-07-12 11:20:51 -0400",

A config/brakeman.yml => config/brakeman.yml +3 -0
@@ 0,0 1,3 @@
---
:skip_checks:
  - CheckPermitAttributes

M config/imagemagick/policy.xml => config/imagemagick/policy.xml +1 -1
@@ 22,6 22,6 @@

  <!-- Disallow any coder by default, and only enable ones required by Mastodon -->
  <policy domain="coder" rights="none" pattern="*" />
  <policy domain="coder" rights="read | write" pattern="{PNG,JPEG,GIF,HEIC,WEBP}" />
  <policy domain="coder" rights="read | write" pattern="{JPEG,PNG,GIF,WEBP,HEIC,AVIF}" />
  <policy domain="coder" rights="write" pattern="{HISTOGRAM,RGB,INFO}" />
</policymap>

A config/initializers/2_limited_federation_mode.rb => config/initializers/2_limited_federation_mode.rb +7 -0
@@ 0,0 1,7 @@
# frozen_string_literal: true

Rails.application.configure do
  config.x.limited_federation_mode = (ENV['LIMITED_FEDERATION_MODE'] || ENV['WHITELIST_MODE']) == 'true'

  warn 'WARN: The environment variable WHITELIST_MODE has been replaced with LIMITED_FEDERATION_MODE, you should rename this environment variable in your configuration.' if ENV.key?('WHITELIST_MODE')
end

D config/initializers/2_whitelist_mode.rb => config/initializers/2_whitelist_mode.rb +0 -5
@@ 1,5 0,0 @@
# frozen_string_literal: true

Rails.application.configure do
  config.x.whitelist_mode = (ENV['LIMITED_FEDERATION_MODE'] || ENV['WHITELIST_MODE']) == 'true'
end

M config/initializers/twitter_regex.rb => config/initializers/twitter_regex.rb +2 -2
@@ 26,9 26,9 @@ module Twitter::TwitterText
        )
      \)
    /iox
    # rubocop:disable
    # rubocop:disable Layout/LineLength
    UCHARS = '\u{A0}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFEF}\u{10000}-\u{1FFFD}\u{20000}-\u{2FFFD}\u{30000}-\u{3FFFD}\u{40000}-\u{4FFFD}\u{50000}-\u{5FFFD}\u{60000}-\u{6FFFD}\u{70000}-\u{7FFFD}\u{80000}-\u{8FFFD}\u{90000}-\u{9FFFD}\u{A0000}-\u{AFFFD}\u{B0000}-\u{BFFFD}\u{C0000}-\u{CFFFD}\u{D0000}-\u{DFFFD}\u{E1000}-\u{EFFFD}\u{E000}-\u{F8FF}\u{F0000}-\u{FFFFD}\u{100000}-\u{10FFFD}'
    # rubocop:enable
    # rubocop:enable Layout/LineLength
    REGEXEN[:valid_url_query_chars] = %r{[a-z0-9!?*'();:&=+$/%#\[\]\-_.,~|@\^#{UCHARS}]}iou
    REGEXEN[:valid_url_query_ending_chars] = %r{[a-z0-9_&=#/\-#{UCHARS}]}iou
    REGEXEN[:valid_url_path] = %r{(?:

M config/navigation.rb => config/navigation.rb +2 -2
@@ 46,7 46,7 @@ SimpleNavigation::Configuration.run do |navigation|
      s.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_path(origin: 'local'), highlights_on: %r{/admin/accounts|/admin/pending_accounts|/admin/disputes|/admin/users}, if: -> { current_user.can?(:manage_users) }
      s.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path, if: -> { current_user.can?(:manage_invites) }
      s.item :follow_recommendations, safe_join([fa_icon('user-plus fw'), t('admin.follow_recommendations.title')]), admin_follow_recommendations_path, highlights_on: %r{/admin/follow_recommendations}, if: -> { current_user.can?(:manage_taxonomies) }
      s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_path(limited: whitelist_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) }
      s.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_path(limited: limited_federation_mode? ? nil : '1'), highlights_on: %r{/admin/instances|/admin/domain_blocks|/admin/domain_allows}, if: -> { current_user.can?(:manage_federation) }
      s.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_path, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.can?(:manage_blocks) }
      s.item :ip_blocks, safe_join([fa_icon('ban fw'), t('admin.ip_blocks.title')]), admin_ip_blocks_path, highlights_on: %r{/admin/ip_blocks}, if: -> { current_user.can?(:manage_blocks) }
      s.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_path, if: -> { current_user.can?(:view_audit_log) }


@@ 60,7 60,7 @@ SimpleNavigation::Configuration.run do |navigation|
      s.item :announcements, safe_join([fa_icon('bullhorn fw'), t('admin.announcements.title')]), admin_announcements_path, highlights_on: %r{/admin/announcements}, if: -> { current_user.can?(:manage_announcements) }
      s.item :custom_emojis, safe_join([fa_icon('smile-o fw'), t('admin.custom_emojis.title')]), admin_custom_emojis_path, highlights_on: %r{/admin/custom_emojis}, if: -> { current_user.can?(:manage_custom_emojis) }
      s.item :webhooks, safe_join([fa_icon('inbox fw'), t('admin.webhooks.title')]), admin_webhooks_path, highlights_on: %r{/admin/webhooks}, if: -> { current_user.can?(:manage_webhooks) }
      s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !whitelist_mode? && current_user.can?(:manage_federation) }
      s.item :relays, safe_join([fa_icon('exchange fw'), t('admin.relays.title')]), admin_relays_path, highlights_on: %r{/admin/relays}, if: -> { !limited_federation_mode? && current_user.can?(:manage_federation) }
    end

    n.item :sidekiq, safe_join([fa_icon('diamond fw'), 'Sidekiq']), sidekiq_path, link_html: { target: 'sidekiq' }, if: -> { current_user.can?(:view_devops) }

M config/webpack/rules/index.js => config/webpack/rules/index.js +2 -0
@@ 1,6 1,7 @@
const babel = require('./babel');
const css = require('./css');
const file = require('./file');
const materialIcons = require('./material_icons');
const nodeModules = require('./node_modules');
const tesseract = require('./tesseract');



@@ 8,6 9,7 @@ const tesseract = require('./tesseract');
// https://webpack.js.org/concepts/loaders/#loader-features
// Lastly, process static files using file loader
module.exports = {
  materialIcons,
  file,
  tesseract,
  css,

A config/webpack/rules/material_icons.js => config/webpack/rules/material_icons.js +13 -0
@@ 0,0 1,13 @@
module.exports = {
  test: /\.svg$/,
  include: /node_modules\/@material-design-icons/,
  issuer: /\.[jt]sx?$/,
  use: [
    {
      loader: '@svgr/webpack',
      options: {
        svgo: false,
      },
    },
  ],
};

M config/webpack/tests.js => config/webpack/tests.js +1 -1
@@ 5,5 5,5 @@ const { merge } = require('webpack-merge');
const sharedConfig = require('./shared');

module.exports = merge(sharedConfig, {
  mode: 'development',
  mode: 'production',
});

M db/migrate/20180812173710_copy_status_stats.rb => db/migrate/20180812173710_copy_status_stats.rb +1 -1
@@ 45,7 45,7 @@ class CopyStatusStats < ActiveRecord::Migration[5.2]
    # We cannot use bulk INSERT or overarching transactions here because of possible
    # uniqueness violations that we need to skip over
    Status.unscoped.select('id, reblogs_count, favourites_count, created_at, updated_at').find_each do |status|
      params = [[nil, status.id], [nil, status.reblogs_count], [nil, status.favourites_count], [nil, status.created_at], [nil, status.updated_at]]
      params = [status.id, status.reblogs_count, status.favourites_count, status.created_at, status.updated_at]
      exec_insert('INSERT INTO status_stats (status_id, reblogs_count, favourites_count, created_at, updated_at) VALUES ($1, $2, $3, $4, $5)', nil, params)
    rescue ActiveRecord::RecordNotUnique
      next

M db/migrate/20181116173541_copy_account_stats.rb => db/migrate/20181116173541_copy_account_stats.rb +1 -1
@@ 45,7 45,7 @@ class CopyAccountStats < ActiveRecord::Migration[5.2]
    # We cannot use bulk INSERT or overarching transactions here because of possible
    # uniqueness violations that we need to skip over
    Account.unscoped.select('id, statuses_count, following_count, followers_count, created_at, updated_at').find_each do |account|
      params = [[nil, account.id], [nil, account[:statuses_count]], [nil, account[:following_count]], [nil, account[:followers_count]], [nil, account.created_at], [nil, account.updated_at]]
      params = [account.id, account[:statuses_count], account[:following_count], account[:followers_count], account.created_at, account.updated_at]
      exec_insert('INSERT INTO account_stats (account_id, statuses_count, following_count, followers_count, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)', nil, params)
    rescue ActiveRecord::RecordNotUnique
      next

M docker-compose.yml => docker-compose.yml +3 -3
@@ 56,7 56,7 @@ services:

  web:
    build: .
    image: ghcr.io/mastodon/mastodon:v4.1.5
    image: ghcr.io/mastodon/mastodon:v4.1.6
    restart: always
    env_file: .env.production
    command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"


@@ 77,7 77,7 @@ services:

  streaming:
    build: .
    image: ghcr.io/mastodon/mastodon:v4.1.5
    image: ghcr.io/mastodon/mastodon:v4.1.6
    restart: always
    env_file: .env.production
    command: node ./streaming


@@ 95,7 95,7 @@ services:

  sidekiq:
    build: .
    image: ghcr.io/mastodon/mastodon:v4.1.5
    image: ghcr.io/mastodon/mastodon:v4.1.6
    restart: always
    env_file: .env.production
    command: bundle exec sidekiq

D lib/action_controller/conditional_get_extensions.rb => lib/action_controller/conditional_get_extensions.rb +0 -15
@@ 1,15 0,0 @@
# frozen_string_literal: true

module ActionController
  module ConditionalGetExtensions
    def expires_in(*)
      # This backports a fix from Rails 7 so that a more private Cache-Control
      # can be overriden by calling expires_in on a specific controller action
      response.cache_control.delete(:no_store)

      super
    end
  end
end

ActionController::ConditionalGet.prepend(ActionController::ConditionalGetExtensions)

M lib/mastodon/cli/statuses.rb => lib/mastodon/cli/statuses.rb +1 -1
@@ 61,7 61,7 @@ module Mastodon::CLI
        # Skip accounts followed by local accounts
        clean_followed_sql = 'AND NOT EXISTS (SELECT 1 FROM follows WHERE statuses.account_id = follows.target_account_id)' unless options[:clean_followed]

        ActiveRecord::Base.connection.exec_insert(<<-SQL.squish, 'SQL', [[nil, max_id]])
        ActiveRecord::Base.connection.exec_insert(<<-SQL.squish, 'SQL', [max_id])
          INSERT INTO statuses_to_be_deleted (id)
          SELECT statuses.id FROM statuses WHERE deleted_at IS NULL AND NOT local AND uri IS NOT NULL AND (id < $1)
          AND NOT EXISTS (SELECT 1 FROM statuses AS statuses1 WHERE statuses.id = statuses1.in_reply_to_id)

M lib/mastodon/version.rb => lib/mastodon/version.rb +1 -1
@@ 13,7 13,7 @@ module Mastodon
    end

    def patch
      5
      6
    end

    def flags

M lib/paperclip/media_type_spoof_detector_extensions.rb => lib/paperclip/media_type_spoof_detector_extensions.rb +3 -1
@@ 2,13 2,15 @@

module Paperclip
  module MediaTypeSpoofDetectorExtensions
    MARCEL_MIME_TYPES = %w(audio/mpeg image/avif).freeze

    def calculated_content_type
      return @calculated_content_type if defined?(@calculated_content_type)

      @calculated_content_type = type_from_file_command.chomp

      # The `file` command fails to recognize some MP3 files as such
      @calculated_content_type = type_from_marcel if @calculated_content_type == 'application/octet-stream' && type_from_marcel == 'audio/mpeg'
      @calculated_content_type = type_from_marcel if @calculated_content_type == 'application/octet-stream' && type_from_marcel.in?(MARCEL_MIME_TYPES)
      @calculated_content_type
    end


M lib/tasks/mastodon.rake => lib/tasks/mastodon.rake +14 -6
@@ 438,12 438,7 @@ namespace :mastodon do
          "#{key}=#{escaped}"
        end.join("\n")

        generated_header = "# Generated with mastodon:setup on #{Time.now.utc}\n\n".dup

        if incompatible_syntax
          generated_header << "# Some variables in this file will be interpreted differently whether you are\n"
          generated_header << "# using docker-compose or not.\n\n"
        end
        generated_header = generate_header(incompatible_syntax)

        Rails.root.join('.env.production').write("#{generated_header}#{env_contents}\n")



@@ 538,6 533,19 @@ namespace :mastodon do
      puts "VAPID_PUBLIC_KEY=#{vapid_key.public_key}"
    end
  end

  private

  def generate_header(include_warning)
    default_message = "# Generated with mastodon:setup on #{Time.now.utc}\n\n"

    default_message.tap do |string|
      if include_warning
        string << "# Some variables in this file will be interpreted differently whether you are\n"
        string << "# using docker-compose or not.\n\n"
      end
    end
  end
end

def disable_log_stdout!

A lib/tasks/spec.rake => lib/tasks/spec.rake +11 -0
@@ 0,0 1,11 @@
# frozen_string_literal: true

if Rake::Task.task_defined?('spec:system')
  namespace :spec do
    task :enable_system_specs do # rubocop:disable Rails/RakeEnvironment
      ENV['RUN_SYSTEM_SPECS'] = 'true'
    end
  end

  Rake::Task['spec:system'].enhance ['spec:enable_system_specs']
end

M package.json => package.json +2 -0
@@ 44,8 44,10 @@
    "@formatjs/intl-pluralrules": "^5.2.2",
    "@gamestdio/websocket": "^0.3.2",
    "@github/webauthn-json": "^2.1.1",
    "@material-design-icons/svg": "^0.14.10",
    "@rails/ujs": "^7.0.6",
    "@reduxjs/toolkit": "^1.9.5",
    "@svgr/webpack": "^5.5.0",
    "abortcontroller-polyfill": "^1.7.5",
    "atrament": "0.2.4",
    "arrow-key-navigation": "^1.2.0",

M spec/controllers/concerns/signature_verification_spec.rb => spec/controllers/concerns/signature_verification_spec.rb +32 -1
@@ 129,6 129,37 @@ describe SignatureVerification do
      end
    end

    context 'with non-normalized URL' do
      before do
        get :success

        fake_request = Request.new(:get, 'http://test.host/subdir/../success')
        fake_request.on_behalf_of(author)

        request.headers.merge!(fake_request.headers)

        allow(controller).to receive(:actor_refresh_key!).and_return(author)
      end

      describe '#build_signed_string' do
        it 'includes the normalized request path' do
          expect(controller.send(:build_signed_string)).to start_with "(request-target): get /success\n"
        end
      end

      describe '#signed_request?' do
        it 'returns true' do
          expect(controller.signed_request?).to be true
        end
      end

      describe '#signed_request_actor' do
        it 'returns an account' do
          expect(controller.signed_request_account).to eq author
        end
      end
    end

    context 'with request with unparsable Date header' do
      before do
        get :success


@@ 202,7 233,7 @@ describe SignatureVerification do

        request.headers.merge!(fake_request.headers)

        stub_request(:get, 'http://localhost:5000/actor#main-key').to_raise(Mastodon::HostValidationError)
        stub_request(:get, 'http://localhost:5000/actor').to_raise(Mastodon::HostValidationError)
      end

      describe '#signed_request?' do

R spec/fabricators_spec.rb => spec/fabricators/fabricators_spec.rb +0 -0
A spec/fixtures/files/600x400.avif => spec/fixtures/files/600x400.avif +0 -0
A spec/fixtures/files/600x400.heic => spec/fixtures/files/600x400.heic +0 -0
A spec/fixtures/files/600x400.jpeg => spec/fixtures/files/600x400.jpeg +0 -0
A spec/fixtures/files/600x400.png => spec/fixtures/files/600x400.png +0 -0
A spec/fixtures/files/600x400.webp => spec/fixtures/files/600x400.webp +0 -0
D spec/fixtures/requests/idn.txt => spec/fixtures/requests/idn.txt +0 -483
@@ 1,483 0,0 @@
HTTP/1.1 200 OK
Server: nginx
Date: Sun, 23 Apr 2017 19:37:13 GMT
Content-Type: text/html
Content-Length: 38111
Last-Modified: Wed, 20 Jul 2016 02:50:52 GMT
Connection: keep-alive
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1" />
		<meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
		<script>
          var _hmt = _hmt || [];
          (function() {
            var hm = document.createElement("script");
            hm.src = "http://hm.baidu.com/hm.js?746c3f6346fae8612933e5921803ee32";
            var s = document.getElementsByTagName("script")[0]; 
            s.parentNode.insertBefore(hm, s);
          })();

    	</script>


		<link rel="stylesheet" type="text/css" href="css/common.css"/>
		<script src="js/jquery-1.11.1.min.js" type="text/javascript" charset="utf-8"></script>
		<script src="js/common.js" type="text/javascript" charset="utf-8"></script>
		<script src="js/carousel.js" type="text/javascript" charset="utf-8"></script>
		<title>中国域名网站</title>

	</head>
	<body>
		<div class="head-tips" id="headTip">
			<span class="close" id="headtips-close"><img src="css/img/close.png" alt="" /></span>
		</div>
		<div class="banner-bg"></div>
		<div class="container">
			<div class="banner">
				<img src="css/img/banner.png" alt="" />
			</div>
			<div class="nav">
				<h1>名站导航</h1>
				<div class="left-btn" id="pre">
					<img src="css/img/arrow-left.png" alt="" />
				</div>
				<div class="carousel">
					<ul class="carousel-content">
						<li>
							<a href="http://中央电视台.中国"  target="_blank">
								<img src="css/img/p10.png" alt="" />
								<p>中央电视台.中国</p>
							</a><a href="http://平安北京.中国"  target="_blank" class="mt-4">
								<img src="css/img/p5.png" alt="" />
								<p>平安北京.中国</p>
							</a>
						</li>
						<li>
							<a href="http://人民网.中国"  target="_blank">
								<img src="css/img/p6.png" alt="" />
								<p>人民网.中国</p>
							</a><a href="http://招商银行.中国"  target="_blank" class="mt-4">
								<img src="css/img/p8.png" alt="" />
								<p>招商银行.中国</p>
							</a>
						</li>
						<li>
							<a href="http://必胜客宅急送.中国"  target="_blank">
								<img src="css/img/p1.png" alt="" />
								<p>必胜客宅急送.中国</p>
							</a><a href="http://创业咖啡.中国"  target="_blank" class="mt-4">
								<img src="css/img/p2.png" alt="" />
								<p>创业咖啡.中国</p>
							</a>
						</li>
						<li>
							<a href="http://中国移动.中国"  target="_blank">
								<img src="css/img/p9.png" alt="" />
								<p>中国移动.中国</p>
							</a><a href="http://海盟.中国"  target="_blank" class="mt-4">
								<img src="css/img/p3.png" alt="" />
								<p>海盟.中国</p>
							</a>
						</li>
						<li>
							<a href="http://艺龙.中国"  target="_blank">
								<img src="css/img/p7.png" alt="" />
								<p>艺龙.中国</p>
							</a><a href="http://和讯.中国"  target="_blank" class="mt-4">
								<img src="css/img/p4.png" alt="" />
								<p>和讯.中国</p>
							</a>
						</li>
					</ul>
				</div>
				<div class="right-btn" id="next">
					<img src="css/img/arrow-right.png" alt="" />
				</div>
			</div>
		</div>
		<div class="all-url">
			<div class="container">
				<h1>网址大全</h1>
				<ul class="url">
					<li><a href="http://人民网.中国" target="_blank">人民网.中国</a></li>
					<li><a href="http://新华网.中国" target="_blank">新华网.中国</a></li>
					<li><a href="http://中央电视台.中国" target="_blank">中央电视台.中国</a></li>
					<li><a href="http://光明网.中国" target="_blank">光明网.中国</a></li>
					<li><a href="http://平安北京.中国" target="_blank">平安北京.中国</a></li>
					<li><a href="http://联想微博.中国" target="_blank">联想微博.中国</a></li>
					<li><a href="http://首都网警.中国" target="_blank">首都网警.中国</a></li>
					<li><a href="http://北京消防.中国" target="_blank">北京消防.中国</a></li>
					<li><a href="http://海淀公安.中国" target="_blank">海淀公安.中国</a></li>
					<li><a href="http://通州警方.中国" target="_blank">通州警方.中国</a></li>
					<li><a href="http://门头沟禁毒.中国" target="_blank">门头沟禁毒.中国</a></li>
					<li><a href="http://西部数码.中国" target="_blank">西部数码.中国</a></li>
					<li><a href="http://中央电视台.中国" target="_blank">中央电视台.中国</a></li>
					<li><a href="http://中国移动.中国" target="_blank">中国移动.中国</a></li>
					<li><a href="http://必胜宅急送.中国" target="_blank">必胜宅急送.中国</a></li>
					<li><a href="http://老正兴.中国" target="_blank">老正兴.中国</a></li>
					<li><a href="http://广州酒家.中国" target="_blank">广州酒家.中国</a></li>
					<li><a href="http://格力.中国" target="_blank">格力.中国</a></li>
					<li><a href="http://福建金爵.中国" target="_blank">福建金爵.中国</a></li>
					<li><a href="http://和信房产.中国" target="_blank">和信房产.中国</a></li>
					<li><a href="http://金爵房地产.中国" target="_blank">金爵房地产.中国</a></li>
					<li><a href="http://联泰地产.中国" target="_blank">联泰地产.中国</a></li>
					<li><a href="http://鲁商置业.中国" target="_blank">鲁商置业.中国</a></li>
					<li><a href="http://鲁商置业股份.中国" target="_blank">鲁商置业股份.中国</a></li>
					<li><a href="http://美佳华.中国" target="_blank">美佳华.中国</a></li>
					<li><a href="http://金世纪工程.中国" target="_blank">金世纪工程.中国</a></li>
					<li><a href="http://金世纪集团.中国" target="_blank">金世纪集团.中国</a></li>
					<li><a href="http://深圳金世纪.中国" target="_blank">深圳金世纪.中国</a></li>
					<li><a href="http://总部基地.中国" target="_blank">总部基地.中国</a></li>
					<li><a href="http://德律风.中国" target="_blank">德律风.中国</a></li>
					<li><a href="http://德律风物业.中国" target="_blank">德律风物业.中国</a></li>
					<li><a href="http://柯林.中国" target="_blank">柯林.中国</a></li>
					<li><a href="http://上海德律风物业.中国" target="_blank">上海德律风物业.中国</a></li>
					<li><a href="http://广东海印集团股份.中国" target="_blank">广东海印集团股份.中国</a></li>
					<li><a href="http://广东海印集团股份有限公司.中国" target="_blank">广东海印集团股份有限公司.中国</a></li>
					<li><a href="http://艺龙.中国" target="_blank">艺龙.中国</a></li>
					<li><a href="http://北京旅游信息网.中国" target="_blank">北京旅游信息网.中国</a></li>
					<li><a href="http://北京故宫博物院.中国" target="_blank">北京故宫博物院.中国</a></li>
					<li><a href="http://旅行张家界.中国" target="_blank">旅行张家界.中国</a></li>
					<li><a href="http://张家界旅游.中国" target="_blank">张家界旅游.中国</a></li>
					<li><a href="http://广州市旅游局.中国" target="_blank">广州市旅游局.中国</a></li>
					<li><a href="http://旅游在线.中国" target="_blank">旅游在线.中国</a></li>
					<li><a href="http://威海旅游集散中心.中国" target="_blank">威海旅游集散中心.中国</a></li>
					<li><a href="http://锦州旅游.中国" target="_blank">锦州旅游.中国</a></li>
					<li><a href="http://金牛湖风景旅游度假区.中国" target="_blank">金牛湖风景旅游度假区.中国</a></li>
					<li><a href="http://环球旅行社.中国" target="_blank">环球旅行社.中国</a></li>
					<li><a href="http://养鹿场.中国" target="_blank">养鹿场.中国</a></li>
					<li><a href="http://东瀛游.中国" target="_blank">东瀛游.中国</a></li>
					<li><a href="http://东瀛游旅行社.中国" target="_blank">东瀛游旅行社.中国</a></li>
					<li><a href="http://桂林游.中国" target="_blank">桂林游.中国</a></li>
					<li><a href="http://桂林之旅.中国" target="_blank">桂林之旅.中国</a></li>
					<li><a href="http://美国环球旅行社.中国" target="_blank">美国环球旅行社.中国</a></li>
					<li><a href="http://东天目山.中国" target="_blank">东天目山.中国</a></li>
					<li><a href="http://凤山寺.中国" target="_blank">凤山寺.中国</a></li>
					<li><a href="http://黄沙古渡.中国" target="_blank">黄沙古渡.中国</a></li>
					<li><a href="http://城头山.中国" target="_blank">城头山.中国</a></li>
					<li><a href="http://港游网.中国" target="_blank">港游网.中国</a></li>
					<li><a href="http://一起游.中国" target="_blank">一起游.中国</a></li>
					<li><a href="http://山水家园.中国" target="_blank">山水家园.中国</a></li>
					<li><a href="http://蒋巷村.中国" target="_blank">蒋巷村.中国</a></li>
					<li><a href="http://蒋巷村农业生态旅游.中国" target="_blank">蒋巷村农业生态旅游.中国</a></li>
					<li><a href="http://厦门海峡旅行社.中国" target="_blank">厦门海峡旅行社.中国</a></li>
					<li><a href="http://姜堰宾馆.中国" target="_blank">姜堰宾馆.中国</a></li>
					<li><a href="http://上海远洋宾馆.中国" target="_blank">上海远洋宾馆.中国</a></li>
					<li><a href="http://红栌山庄.中国" target="_blank">红栌山庄.中国</a></li>
					<li><a href="http://金牛湖风景旅游度假区.中国" target="_blank">金牛湖风景旅游度假区.中国</a></li>
					<li><a href="http://金牛湖风景区.中国" target="_blank">金牛湖风景区.中国</a></li>
					<li><a href="http://北京半岛酒店.中国" target="_blank">北京半岛酒店.中国</a></li>
					<li><a href="http://比华利山半岛酒店.中国" target="_blank">比华利山半岛酒店.中国</a></li>
					<li><a href="http://东京半岛酒店.中国" target="_blank">东京半岛酒店.中国</a></li>
					<li><a href="http://君乐酒店.中国" target="_blank">君乐酒店.中国</a></li>
					<li><a href="http://凯迪威酒店.中国" target="_blank">凯迪威酒店.中国</a></li>
					<li><a href="http://莱州酒店.中国" target="_blank">莱州酒店.中国</a></li>
					<li><a href="http://曼谷半岛酒店.中国" target="_blank">曼谷半岛酒店.中国</a></li>
					<li><a href="http://上海半岛酒店.中国" target="_blank">上海半岛酒店.中国</a></li>
					<li><a href="http://上虞国际大酒店.中国" target="_blank">上虞国际大酒店.中国</a></li>
					<li><a href="http://王府半島酒店.中国" target="_blank">王府半島酒店.中国</a></li>
					<li><a href="http://香港半岛酒店.中国" target="_blank">香港半岛酒店.中国</a></li>
					<li><a href="http://银河大酒店.中国" target="_blank">银河大酒店.中国</a></li>
					<li><a href="http://健康365.中国" target="_blank">健康365.中国</a></li>
					<li><a href="http://家天下.中国" target="_blank">家天下.中国</a></li>
					<li><a href="http://北京大学第三医院.中国" target="_blank">北京大学第三医院.中国</a></li>
					<li><a href="http://西藏阜康医药.中国" target="_blank">西藏阜康医药.中国</a></li>
					<li><a href="http://沈阳妇婴医院.中国" target="_blank">沈阳妇婴医院.中国</a></li>
					<li><a href="http://福建医科大学附属第一医院.中国" target="_blank">福建医科大学附属第一医院.中国</a></li>
					<li><a href="http://北方药业.中国" target="_blank">北方药业.中国</a></li>
					<li><a href="http://医药导报.中国" target="_blank">医药导报.中国</a></li>
					<li><a href="http://中国医药导报.中国" target="_blank">中国医药导报.中国</a></li>
					<li><a href="http://云南省医药有限公司.中国" target="_blank">云南省医药有限公司.中国</a></li>
					<li><a href="http://云南省医药.中国" target="_blank">云南省医药.中国</a></li>
					<li><a href="http://必胜宅急送.中国" target="_blank">必胜宅急送.中国</a></li>
					<li><a href="http://青岛啤酒股份有限公司.中国" target="_blank">青岛啤酒股份有限公司.中国</a></li>
					<li><a href="http://火锅面.中国" target="_blank">火锅面.中国</a></li>
					<li><a href="http://57度湘.中国" target="_blank">57度湘.中国</a></li>
					<li><a href="http://澳門佳景集團.中国" target="_blank">澳門佳景集團.中国</a></li>
					<li><a href="http://澳門佳景飲食集團.中国" target="_blank">澳門佳景飲食集團.中国</a></li>
					<li><a href="http://赤峰陈曲.中国" target="_blank">赤峰陈曲.中国</a></li>
					<li><a href="http://春宝.中国" target="_blank">春宝.中国</a></li>
					<li><a href="http://富农水稻.中国" target="_blank">富农水稻.中国</a></li>
					<li><a href="http://功德林.中国" target="_blank">功德林.中国</a></li>
					<li><a href="http://古船.中国" target="_blank">古船.中国</a></li>
					<li><a href="http://古船食品.中国" target="_blank">古船食品.中国</a></li>
					<li><a href="http://红岩村.中国" target="_blank">红岩村.中国</a></li>
					<li><a href="http://佳景飲食集團.中国" target="_blank">佳景飲食集團.中国</a></li>
					<li><a href="http://赖永初酒业.中国" target="_blank">赖永初酒业.中国</a></li>
					<li><a href="http://厉家菜.中国" target="_blank">厉家菜.中国</a></li>
					<li><a href="http://莲花岛.中国" target="_blank">莲花岛.中国</a></li>
					<li><a href="http://廖平一两酒.中国" target="_blank">廖平一两酒.中国</a></li>
					<li><a href="http://龙轩.中国" target="_blank">龙轩.中国</a></li>
					<li><a href="http://迈德乐.中国" target="_blank">迈德乐.中国</a></li>
					<li><a href="http://明记炖品.中国" target="_blank">明记炖品.中国</a></li>
					<li><a href="http://明记炖品世家.中国" target="_blank">明记炖品世家.中国</a></li>
					<li><a href="http://黔江鸡杂.中国" target="_blank">黔江鸡杂.中国</a></li>
					<li><a href="http://聖安娜餅屋.中国" target="_blank">聖安娜餅屋.中国</a></li>
					<li><a href="http://华夏茶业网.中国" target="_blank">华夏茶业网.中国</a></li>
					<li><a href="http://宅香锅.中国" target="_blank">宅香锅.中国</a></li>
					<li><a href="http://荞麦面.中国" target="_blank">荞麦面.中国</a></li>
					<li><a href="http://宅面坊.中国" target="_blank">宅面坊.中国</a></li>
					<li><a href="http://宅豆坊.中国" target="_blank">宅豆坊.中国</a></li>
					<li><a href="http://草原羔羊肉.中国" target="_blank">草原羔羊肉.中国</a></li>
					<li><a href="http://火锅饺.中国" target="_blank">火锅饺.中国</a></li>
					<li><a href="http://鸟鸡蛋.中国" target="_blank">鸟鸡蛋.中国</a></li>
					<li><a href="http://宅米饭.中国" target="_blank">宅米饭.中国</a></li>
					<li><a href="http://白野猪肉.中国" target="_blank">白野猪肉.中国</a></li>
					<li><a href="http://黑野猪肉.中国" target="_blank">黑野猪肉.中国</a></li>
					<li><a href="http://特色野猪肉.中国" target="_blank">特色野猪肉.中国</a></li>
					<li><a href="http://生态畜牧.中国" target="_blank">生态畜牧.中国</a></li>
					<li><a href="http://野豆坊.中国" target="_blank">野豆坊.中国</a></li>
					<li><a href="http://野猪牧.中国" target="_blank">野猪牧.中国</a></li>
					<li><a href="http://野猪网.中国" target="_blank">野猪网.中国</a></li>
					<li><a href="http://酷牛肉.中国" target="_blank">酷牛肉.中国</a></li>
					<li><a href="http://羔羊网.中国" target="_blank">羔羊网.中国</a></li>
					<li><a href="http://野猪肉.中国" target="_blank">野猪肉.中国</a></li>
					<li><a href="http://鸟鸡肉.中国" target="_blank">鸟鸡肉.中国</a></li>
					<li><a href="http://藏羔羊.中国" target="_blank">藏羔羊.中国</a></li>
					<li><a href="http://酷牛牧场.中国" target="_blank">酷牛牧场.中国</a></li>
					<li><a href="http://鸟鸡牧场.中国" target="_blank">鸟鸡牧场.中国</a></li>
					<li><a href="http://鸟鸡网.中国" target="_blank">鸟鸡网.中国</a></li>
					<li><a href="http://家餐馆.中国" target="_blank">家餐馆.中国</a></li>
					<li><a href="http://宅火锅.中国" target="_blank">宅火锅.中国</a></li>
					<li><a href="http://食品饮料网.中国" target="_blank">食品饮料网.中国</a></li>
					<li><a href="http://中国湿巾.中国" target="_blank">中国湿巾.中国</a></li>
					<li><a href="http://海特果菜.中国" target="_blank">海特果菜.中国</a></li>
					<li><a href="http://果菜.中国" target="_blank">果菜.中国</a></li>
					<li><a href="http://宏鑫德.中国" target="_blank">宏鑫德.中国</a></li>
					<li><a href="http://北方烧酒.中国" target="_blank">北方烧酒.中国</a></li>
					<li><a href="http://欧兰娑曼.中国" target="_blank">欧兰娑曼.中国</a></li>
					<li><a href="http://威尔富.中国" target="_blank">威尔富.中国</a></li>
					<li><a href="http://虎林老窖.中国" target="_blank">虎林老窖.中国</a></li>
					<li><a href="http://唐记食品.中国" target="_blank">唐记食品.中国</a></li>
					<li><a href="http://津恺食品.中国" target="_blank">津恺食品.中国</a></li>
					<li><a href="http://津恺.中国" target="_blank">津恺.中国</a></li>
					<li><a href="http://老中医养生.中国" target="_blank">老中医养生.中国</a></li>
					<li><a href="http://山东伟龙食品公司.中国" target="_blank">山东伟龙食品公司.中国</a></li>
					<li><a href="http://太泉蜂业.中国" target="_blank">太泉蜂业.中国</a></li>
					<li><a href="http://天鹅肉.中国" target="_blank">天鹅肉.中国</a></li>
					<li><a href="http://望湘园.中国" target="_blank">望湘园.中国</a></li>
					<li><a href="http://伟龙饼干.中国" target="_blank">伟龙饼干.中国</a></li>
					<li><a href="http://沃根葡萄酒.中国" target="_blank">沃根葡萄酒.中国</a></li>
					<li><a href="http://亚坤集团.中国" target="_blank">亚坤集团.中国</a></li>
					<li><a href="http://鱼丸.中国" target="_blank">鱼丸.中国</a></li>
					<li><a href="http://真美集团.中国" target="_blank">真美集团.中国</a></li>
					<li><a href="http://真美食品.中国" target="_blank">真美食品.中国</a></li>
					<li><a href="http://中国餐饮标识.中国" target="_blank">中国餐饮标识.中国</a></li>
					<li><a href="http://迷奇.中国" target="_blank">迷奇.中国</a></li>
					<li><a href="http://乐隆隆.中国" target="_blank">乐隆隆.中国</a></li>
					<li><a href="http://绞股蓝.中国" target="_blank">绞股蓝.中国</a></li>
					<li><a href="http://瀑布仙茗.中国" target="_blank">瀑布仙茗.中国</a></li>
					<li><a href="http://金记食品.中国" target="_blank">金记食品.中国</a></li>
					<li><a href="http://朱老六.中国" target="_blank">朱老六.中国</a></li>
					<li><a href="http://嘉太.中国" target="_blank">嘉太.中国</a></li>
					<li><a href="http://顺德堂.中国" target="_blank">顺德堂.中国</a></li>
					<li><a href="http://广味源.中国" target="_blank">广味源.中国</a></li>
					<li><a href="http://德辉食品.中国" target="_blank">德辉食品.中国</a></li>
					<li><a href="http://金龙船.中国" target="_blank">金龙船.中国</a></li>
					<li><a href="http://东方即白.中国" target="_blank">东方即白.中国</a></li>
					<li><a href="http://中山华美实业.中国" target="_blank">中山华美实业.中国</a></li>
					<li><a href="http://富士亭.中国" target="_blank">富士亭.中国</a></li>
					<li><a href="http://三安科技.中国" target="_blank">三安科技.中国</a></li>
					<li><a href="http://供美香食品.中国" target="_blank">供美香食品.中国</a></li>
					<li><a href="http://丰德天元.中国" target="_blank">丰德天元.中国</a></li>
					<li><a href="http://老藏医.中国" target="_blank">老藏医.中国</a></li>
					<li><a href="http://新农仓.中国" target="_blank">新农仓.中国</a></li>
					<li><a href="http://濠吉.中国" target="_blank">濠吉.中国</a></li>
					<li><a href="http://品味爽.中国" target="_blank">品味爽.中国</a></li>
					<li><a href="http://坤育.中国" target="_blank">坤育.中国</a></li>
					<li><a href="http://皇宫食品.中国" target="_blank">皇宫食品.中国</a></li>
					<li><a href="http://依海.中国" target="_blank">依海.中国</a></li>
					<li><a href="http://广州凯虹.中国" target="_blank">广州凯虹.中国</a></li>
					<li><a href="http://宝姿日化.中国" target="_blank">宝姿日化.中国</a></li>
					<li><a href="http://乐高乐.中国" target="_blank">乐高乐.中国</a></li>
					<li><a href="http://茂华食品.中国" target="_blank">茂华食品.中国</a></li>
					<li><a href="http://白鹿集团.中国" target="_blank">白鹿集团.中国</a></li>
					<li><a href="http://好丽友集团.中国" target="_blank">好丽友集团.中国</a></li>
					<li><a href="http://法兰红.中国" target="_blank">法兰红.中国</a></li>
					<li><a href="http://教育部.中国" target="_blank">教育部.中国</a></li>
					<li><a href="http://国家民委.中国" target="_blank">国家民委.中国</a></li>
					<li><a href="http://人口计生委.中国" target="_blank">人口计生委.中国</a></li>
					<li><a href="http://工商总局.中国" target="_blank">工商总局.中国</a></li>
					<li><a href="http://监察部.中国" target="_blank">监察部.中国</a></li>
					<li><a href="http://农业部.中国" target="_blank">农业部.中国</a></li>
					<li><a href="http://人民银行.中国" target="_blank">人民银行.中国</a></li>
					<li><a href="http://侨办.中国" target="_blank">侨办.中国</a></li>
					<li><a href="http://食品药品监督局.中国" target="_blank">食品药品监督局.中国</a></li>
					<li><a href="http://科技部.中国" target="_blank">科技部.中国</a></li>
					<li><a href="http://财政部.中国" target="_blank">财政部.中国</a></li>
					<li><a href="http://文化部.中国" target="_blank">文化部.中国</a></li>
					<li><a href="http://审计署.中国" target="_blank">审计署.中国</a></li>
					<li><a href="http://体育总局.中国" target="_blank">体育总局.中国</a></li>
					<li><a href="http://知识产权局.中国" target="_blank">知识产权局.中国</a></li>
					<li><a href="http://国研网.中国" target="_blank">国研网.中国</a></li>
					<li><a href="http://电监会.中国" target="_blank">电监会.中国</a></li>
					<li><a href="http://民航总局.中国" target="_blank">民航总局.中国</a></li>
					<li><a href="http://卫生部.中国" target="_blank">卫生部.中国</a></li>
					<li><a href="http://安全监察总局.中国" target="_blank">安全监察总局.中国</a></li>
					<li><a href="http://国家行政学院.中国" target="_blank">国家行政学院.中国</a></li>
					<li><a href="http://申银万国.中国" target="_blank">申银万国.中国</a></li>
					<li><a href="http://保定保险协会.中国" target="_blank">保定保险协会.中国</a></li>
					<li><a href="http://和讯.中国" target="_blank">和讯.中国</a></li>
					<li><a href="http://招商证券.中国" target="_blank">招商证券.中国</a></li>
					<li><a href="http://中投证券.中国" target="_blank">中投证券.中国</a></li>
					<li><a href="http://鹏元征信.中国" target="_blank">鹏元征信.中国</a></li>
					<li><a href="http://中融联合.中国" target="_blank">中融联合.中国</a></li>
					<li><a href="http://长城资产.中国" target="_blank">长城资产.中国</a></li>
					<li><a href="http://周生生證券.中国" target="_blank">周生生證券.中国</a></li>
					<li><a href="http://福建湄洲湾控股.中国" target="_blank">福建湄洲湾控股.中国</a></li>
					<li><a href="http://中安现金.中国" target="_blank">中安现金.中国</a></li>
					<li><a href="http://中安信业.中国" target="_blank">中安信业.中国</a></li>
					<li><a href="http://聯訊證券.中国" target="_blank">聯訊證券.中国</a></li>
					<li><a href="http://元富理財网.中国" target="_blank">元富理財网.中国</a></li>
					<li><a href="http://金立方资本.中国" target="_blank">金立方资本.中国</a></li>
					<li><a href="http://安信证券.中国" target="_blank">安信证券.中国</a></li>
					<li><a href="http://中国创业投资网.中国" target="_blank">中国创业投资网.中国</a></li>
					<li><a href="http://進邦匯理.中国" target="_blank">進邦匯理.中国</a></li>
					<li><a href="http://中再集团.中国" target="_blank">中再集团.中国</a></li>
					<li><a href="http://交通银行.中国" target="_blank">交通银行.中国</a></li>
					<li><a href="http://农业银行.中国" target="_blank">农业银行.中国</a></li>
					<li><a href="http://民生银行.中国" target="_blank">民生银行.中国</a></li>
					<li><a href="http://招商银行.中国" target="_blank">招商银行.中国</a></li>
					<li><a href="http://黄河银行.中国" target="_blank">黄河银行.中国</a></li>
					<li><a href="http://周口市商业银行.中国" target="_blank">周口市商业银行.中国</a></li>
					<li><a href="http://金融快线.中国" target="_blank">金融快线.中国</a></li>
					<li><a href="http://农信银.中国" target="_blank">农信银.中国</a></li>
					<li><a href="http://乐pad微博.中国" target="_blank">乐pad微博.中国</a></li>
					<li><a href="http://联想显示器.中国" target="_blank">联想显示器.中国</a></li>
					<li><a href="http://联想打印.中国" target="_blank">联想打印.中国</a></li>
					<li><a href="http://联想Z流行.中国" target="_blank">联想Z流行.中国</a></li>
					<li><a href="http://中国国际新闻网.中国" target="_blank">中国国际新闻网.中国</a></li>
					<li><a href="http://洛阳电视台.中国" target="_blank">洛阳电视台.中国</a></li>
					<li><a href="http://崇左新闻网.中国" target="_blank">崇左新闻网.中国</a></li>
					<li><a href="http://超越之路.中国" target="_blank">超越之路.中国</a></li>
					<li><a href="http://长安教育网.中国" target="_blank">长安教育网.中国</a></li>
					<li><a href="http://唐密茶道.中国" target="_blank">唐密茶道.中国</a></li>
					<li><a href="http://雷峰陪练.中国" target="_blank">雷峰陪练.中国</a></li>
					<li><a href="http://考研.中国" target="_blank">考研.中国</a></li>
					<li><a href="http://世界大学城.中国" target="_blank">世界大学城.中国</a></li>
					<li><a href="http://路正驾校.中国" target="_blank">路正驾校.中国</a></li>
					<li><a href="http://比特威.中国" target="_blank">比特威.中国</a></li>
					<li><a href="http://吉林省农业科学院.中国" target="_blank">吉林省农业科学院.中国</a></li>
					<li><a href="http://普通话审音.中国" target="_blank">普通话审音.中国</a></li>
					<li><a href="http://童帅国际教育.中国" target="_blank">童帅国际教育.中国</a></li>
					<li><a href="http://成功之钥.中国" target="_blank">成功之钥.中国</a></li>
					<li><a href="http://西安理工大学.中国" target="_blank">西安理工大学.中国</a></li>
					<li><a href="http://贵阳电脑学校.中国" target="_blank">贵阳电脑学校.中国</a></li>
					<li><a href="http://黑龙江省实验中学.中国" target="_blank">黑龙江省实验中学.中国</a></li>
					<li><a href="http://浙江艺术职业学院.中国" target="_blank">浙江艺术职业学院.中国</a></li>
					<li><a href="http://萃忆学堂.中国" target="_blank">萃忆学堂.中国</a></li>
					<li><a href="http://闽南科技学院.中国" target="_blank">闽南科技学院.中国</a></li>
					<li><a href="http://普通话语音.中国" target="_blank">普通话语音.中国</a></li>
					<li><a href="http://鞍山师范大学.中国" target="_blank">鞍山师范大学.中国</a></li>
					<li><a href="http://北京电影学院.中国" target="_blank">北京电影学院.中国</a></li>
					<li><a href="http://成都理工大学.中国" target="_blank">成都理工大学.中国</a></li>
					<li><a href="http://东北大学.中国" target="_blank">东北大学.中国</a></li>
					<li><a href="http://赣南师范学院.中国" target="_blank">赣南师范学院.中国</a></li>
					<li><a href="http://广州大学.中国" target="_blank">广州大学.中国</a></li>
					<li><a href="http://河北大学.中国" target="_blank">河北大学.中国</a></li>
					<li><a href="http://河北科技师范学院.中国" target="_blank">河北科技师范学院.中国</a></li>
					<li><a href="http://河南农业大学.中国" target="_blank">河南农业大学.中国</a></li>
					<li><a href="http://江西师范大学.中国" target="_blank">江西师范大学.中国</a></li>
					<li><a href="http://辽宁大学.中国" target="_blank">辽宁大学.中国</a></li>
					<li><a href="http://南昌大学.中国" target="_blank">南昌大学.中国</a></li>
					<li><a href="http://南京理工大学.中国" target="_blank">南京理工大学.中国</a></li>
					<li><a href="http://青岛大学.中国" target="_blank">青岛大学.中国</a></li>
					<li><a href="http://山东大学.中国" target="_blank">山东大学.中国</a></li>
					<li><a href="http://汕头大学.中国" target="_blank">汕头大学.中国</a></li>
					<li><a href="http://上海交通大学.中国" target="_blank">上海交通大学.中国</a></li>
					<li><a href="http://首都经济贸易大学.中国" target="_blank">首都经济贸易大学.中国</a></li>
					<li><a href="http://四川文理学院.中国" target="_blank">四川文理学院.中国</a></li>
					<li><a href="http://天津大学.中国" target="_blank">天津大学.中国</a></li>
					<li><a href="http://五邑大学.中国" target="_blank">五邑大学.中国</a></li>
					<li><a href="http://百色学院.中国" target="_blank">百色学院.中国</a></li>
					<li><a href="http://北京化工大学.中国" target="_blank">北京化工大学.中国</a></li>
					<li><a href="http://大连理工大学.中国" target="_blank">大连理工大学.中国</a></li>
					<li><a href="http://福建医科大学.中国" target="_blank">福建医科大学.中国</a></li>
					<li><a href="http://广东工业大学.中国" target="_blank">广东工业大学.中国</a></li>
					<li><a href="http://海南师范大学.中国" target="_blank">海南师范大学.中国</a></li>
					<li><a href="http://淮海工学院.中国" target="_blank">淮海工学院.中国</a></li>
					<li><a href="http://辽宁对外经贸学院.中国" target="_blank">辽宁对外经贸学院.中国</a></li>
					<li><a href="http://青海师范大学.中国" target="_blank">青海师范大学.中国</a></li>
					<li><a href="http://山东农业大学.中国" target="_blank">山东农业大学.中国</a></li>
					<li><a href="http://上海财经大学.中国" target="_blank">上海财经大学.中国</a></li>
					<li><a href="http://上海中医药大学.中国" target="_blank">上海中医药大学.中国</a></li>
					<li><a href="http://首都师范大学.中国" target="_blank">首都师范大学.中国</a></li>
					<li><a href="http://塔里木大学.中国" target="_blank">塔里木大学.中国</a></li>
					<li><a href="http://西安电子科技大学.中国" target="_blank">西安电子科技大学.中国</a></li>
					<li><a href="http://清华大学.中国" target="_blank">清华大学.中国</a></li>
					<li><a href="http://大连医科大学.中国" target="_blank">大连医科大学.中国</a></li>
					<li><a href="http://贵州大学.中国" target="_blank">贵州大学.中国</a></li>
					<li><a href="http://哈尔滨学院.中国" target="_blank">哈尔滨学院.中国</a></li>
					<li><a href="http://海南医学院.中国" target="_blank">海南医学院.中国</a></li>
					<li><a href="http://黑龙江大学.中国" target="_blank">黑龙江大学.中国</a></li>
					<li><a href="http://集美大学.中国" target="_blank">集美大学.中国</a></li>
					<li><a href="http://南京邮电大学.中国" target="_blank">南京邮电大学.中国</a></li>
					<li><a href="http://上海大学.中国" target="_blank">上海大学.中国</a></li>
					<li><a href="http://深圳大学.中国" target="_blank">深圳大学.中国</a></li>
					<li><a href="http://四川大学.中国" target="_blank">四川大学.中国</a></li>
					<li><a href="http://天津师范大学.中国" target="_blank">天津师范大学.中国</a></li>
					<li><a href="http://西安工业大学.中国" target="_blank">西安工业大学.中国</a></li>
					<li><a href="http://北华大学.中国" target="_blank">北华大学.中国</a></li>
					<li><a href="http://防灾科技学院.中国" target="_blank">防灾科技学院.中国</a></li>
					<li><a href="http://甘肃农业大学.中国" target="_blank">甘肃农业大学.中国</a></li>
					<li><a href="http://广西师范学院.中国" target="_blank">广西师范学院.中国</a></li>
					<li><a href="http://哈尔滨医科大学.中国" target="_blank">哈尔滨医科大学.中国</a></li>
					<li><a href="http://河北科技大学.中国" target="_blank">河北科技大学.中国</a></li>
					<li><a href="http://内蒙古大学.中国" target="_blank">内蒙古大学.中国</a></li>
					<li><a href="http://宁夏大学.中国" target="_blank">宁夏大学.中国</a></li>
					<li><a href="http://山东财经大学.中国" target="_blank">山东财经大学.中国</a></li>
					<li><a href="http://陕西师范大学.中国" target="_blank">陕西师范大学.中国</a></li>
					<li><a href="http://上海对外贸易学院.中国" target="_blank">上海对外贸易学院.中国</a></li>
					<li><a href="http://四川警察学院.中国" target="_blank">四川警察学院.中国</a></li>
					<li><a href="http://西华大学.中国" target="_blank">西华大学.中国</a></li>
					<li><a href="http://许昌学院.中国" target="_blank">许昌学院.中国</a></li>
					<li><a href="http://扬州大学.中国" target="_blank">扬州大学.中国</a></li>
					<li><a href="http://中国矿业大学.中国" target="_blank">中国矿业大学.中国</a></li>
					<li><a href="http://中南大学.中国" target="_blank">中南大学.中国</a></li>
					<li><a href="http://西安理工大学.中国" target="_blank">西安理工大学.中国</a></li>
					<li><a href="http://烟台大学.中国" target="_blank">烟台大学.中国</a></li>
					<li><a href="http://漳州师范学院.中国" target="_blank">漳州师范学院.中国</a></li>
					<li><a href="http://郑州大学.中国" target="_blank">郑州大学.中国</a></li>
					<li><a href="http://中国农业大学.中国" target="_blank">中国农业大学.中国</a></li>
					<li><a href="http://中国医药大学.中国" target="_blank">中国医药大学.中国</a></li>
					<li><a href="http://西安邮电学院.中国" target="_blank">西安邮电学院.中国</a></li>
					<li><a href="http://新疆大学.中国" target="_blank">新疆大学.中国</a></li>
					<li><a href="http://云南师范大学.中国" target="_blank">云南师范大学.中国</a></li>
					<li><a href="http://中国政法大学.中国" target="_blank">中国政法大学.中国</a></li>
					<li><a href="http://西昌学院.中国" target="_blank">西昌学院.中国</a></li>
					<li><a href="http://新疆农业大学.中国" target="_blank">新疆农业大学.中国</a></li>
					<li><a href="http://浙江万里学院.中国" target="_blank">浙江万里学院.中国</a></li>
					<li><a href="http://重庆大学.中国" target="_blank">重庆大学.中国</a></li>

				</ul>
			</div>
		</div>
		<div class="open">
		</div>
		<div class="container">
			<h1 class="Chinese-domain">中文域名简介</h1>
			<p class="Chinese-domain-content">
				“中国域名”是中文域名的一种,特指以“中国”为后缀的中文域名,是我国域名体系和全球互联网域名体系的重要组成部分。“中国”是在全球互联网上代表中国的中文顶级域名,于2010年7月正式纳入全球互联网域名体系,全球互联网域名体系,全球网民可通过联网计算机在世界任何国家和地区实现无障碍访问。“中国”域名在使用上和 .CN,相似属于互联网上的基础服务,基于域名可以提供WWW.EMAIL FTP等应用服务。
			</p>
		</div>
		<div class="footer">
			<p>ICP备案编号:京ICP 备09112257号-68  版权所有中国互联网信息中心</p>
		</div>
	</body>
	<script>
	$("#headTip").hide()
	var hostname = window.location.hostname || "";

	var tips =  "您所访问的域名 <font size='' color='#ff0000'>" + hostname +"</font> 无法到达,您可以尝试重新访问,或使用搜索相关信息"
	if (hostname != "导航.中国") {
		$("#headTip").html(tips);
		$("#headTip").delay(500).slideDown();
		$('#headTip').delay(5000).slideUp();
	}
	</script>
</html>

D spec/helpers/admin/action_logs_helper_spec.rb => spec/helpers/admin/action_logs_helper_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Admin::ActionLogsHelper do
end

M spec/lib/request_spec.rb => spec/lib/request_spec.rb +149 -1
@@ 4,7 4,9 @@ require 'rails_helper'
require 'securerandom'

describe Request do
  subject { described_class.new(:get, 'http://example.com') }
  subject { described_class.new(:get, url) }

  let(:url) { 'http://example.com' }

  describe '#headers' do
    it 'returns user agent' do


@@ 92,6 94,152 @@ describe Request do
        expect { subject.perform }.to raise_error Mastodon::ValidationError
      end
    end

    context 'with bare domain URL' do
      let(:url) { 'http://example.com' }

      before do
        stub_request(:get, 'http://example.com')
      end

      it 'normalizes path' do
        subject.perform do |response|
          expect(response.request.uri.path).to eq '/'
        end
      end

      it 'normalizes path used for request signing' do
        subject.perform

        headers = subject.instance_variable_get(:@headers)
        expect(headers[Request::REQUEST_TARGET]).to eq 'get /'
      end

      it 'normalizes path used in request line' do
        subject.perform do |response|
          expect(response.request.headline).to eq 'GET / HTTP/1.1'
        end
      end
    end

    context 'with unnormalized URL' do
      let(:url) { 'HTTP://EXAMPLE.com:80/foo%41%3A?bar=%41%3A#baz' }

      before do
        stub_request(:get, 'http://example.com/foo%41%3A?bar=%41%3A')
      end

      it 'normalizes scheme' do
        subject.perform do |response|
          expect(response.request.uri.scheme).to eq 'http'
        end
      end

      it 'normalizes host' do
        subject.perform do |response|
          expect(response.request.uri.authority).to eq 'example.com'
        end
      end

      it 'does not modify path' do
        subject.perform do |response|
          expect(response.request.uri.path).to eq '/foo%41%3A'
        end
      end

      it 'does not modify query string' do
        subject.perform do |response|
          expect(response.request.uri.query).to eq 'bar=%41%3A'
        end
      end

      it 'does not modify path used for request signing' do
        subject.perform

        headers = subject.instance_variable_get(:@headers)
        expect(headers[Request::REQUEST_TARGET]).to eq 'get /foo%41%3A'
      end

      it 'does not modify path used in request line' do
        subject.perform do |response|
          expect(response.request.headline).to eq 'GET /foo%41%3A?bar=%41%3A HTTP/1.1'
        end
      end

      it 'strips fragment' do
        subject.perform do |response|
          expect(response.request.uri.fragment).to be_nil
        end
      end
    end

    context 'with non-ASCII URL' do
      let(:url) { 'http://éxample.com:81/föo?bär=1' }

      before do
        stub_request(:get, 'http://xn--xample-9ua.com:81/f%C3%B6o?b%C3%A4r=1')
      end

      it 'IDN-encodes host' do
        subject.perform do |response|
          expect(response.request.uri.authority).to eq 'xn--xample-9ua.com:81'
        end
      end

      it 'IDN-encodes host in Host header' do
        subject.perform do |response|
          expect(response.request.headers['Host']).to eq 'xn--xample-9ua.com'
        end
      end

      it 'percent-escapes path used for request signing' do
        subject.perform

        headers = subject.instance_variable_get(:@headers)
        expect(headers[Request::REQUEST_TARGET]).to eq 'get /f%C3%B6o'
      end

      it 'normalizes path used in request line' do
        subject.perform do |response|
          expect(response.request.headline).to eq 'GET /f%C3%B6o?b%C3%A4r=1 HTTP/1.1'
        end
      end
    end

    context 'with redirecting URL' do
      let(:url) { 'http://example.com/foo' }

      before do
        stub_request(:get, 'http://example.com/foo').to_return(status: 302, headers: { 'Location' => 'HTTPS://EXAMPLE.net/Bar' })
        stub_request(:get, 'https://example.net/Bar').to_return(body: 'Lorem ipsum')
      end

      it 'resolves redirect' do
        subject.perform do |response|
          expect(response.body.to_s).to eq 'Lorem ipsum'
        end

        expect(a_request(:get, 'https://example.net/Bar')).to have_been_made
      end

      it 'normalizes destination scheme' do
        subject.perform do |response|
          expect(response.request.uri.scheme).to eq 'https'
        end
      end

      it 'normalizes destination host' do
        subject.perform do |response|
          expect(response.request.uri.authority).to eq 'example.net'
        end
      end

      it 'does modify path' do
        subject.perform do |response|
          expect(response.request.uri.path).to eq '/Bar'
        end
      end
    end
  end

  describe "response's body_with_limit method" do

M spec/mailers/notification_mailer_spec.rb => spec/mailers/notification_mailer_spec.rb +32 -11
@@ 3,21 3,42 @@
require 'rails_helper'

RSpec.describe NotificationMailer do
  let(:receiver)       { Fabricate(:user) }
  let(:receiver)       { Fabricate(:user, account_attributes: { username: 'alice' }) }
  let(:sender)         { Fabricate(:account, username: 'bob') }
  let(:foreign_status) { Fabricate(:status, account: sender, text: 'The body of the foreign status') }
  let(:own_status)     { Fabricate(:status, account: receiver.account, text: 'The body of the own status') }

  shared_examples 'headers' do |type, thread|
    it 'renders the to and from headers' do
      expect(mail[:to].value).to eq "#{receiver.account.username} <#{receiver.email}>"
      expect(mail.from).to eq ['notifications@localhost']
    end

    it 'renders the list headers' do
      expect(mail['List-ID'].value).to eq "<#{type}.alice.cb6e6126.ngrok.io>"
      expect(mail['List-Unsubscribe'].value).to match(%r{<https://cb6e6126.ngrok.io/unsubscribe\?token=.+>})
      expect(mail['List-Unsubscribe'].value).to match("&type=#{type}")
      expect(mail['List-Unsubscribe-Post'].value).to eq 'List-Unsubscribe=One-Click'
    end

    if thread
      it 'renders the thread headers' do
        expect(mail['In-Reply-To'].value).to match(/<conversation-\d+.\d\d\d\d-\d\d-\d\d@cb6e6126.ngrok.io>/)
        expect(mail['References'].value).to match(/<conversation-\d+.\d\d\d\d-\d\d-\d\d@cb6e6126.ngrok.io>/)
      end
    end
  end

  describe 'mention' do
    let(:mention) { Mention.create!(account: receiver.account, status: foreign_status) }
    let(:notification) { Notification.create!(account: receiver.account, activity: mention) }
    let(:mail) { prepared_mailer_for(receiver.account).mention }

    include_examples 'localized subject', 'notification_mailer.mention.subject', name: 'bob'
    include_examples 'headers', 'mention', true

    it 'renders the headers' do
    it 'renders the subject' do
      expect(mail.subject).to eq('You were mentioned by bob')
      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
    end

    it 'renders the body' do


@@ 32,10 53,10 @@ RSpec.describe NotificationMailer do
    let(:mail) { prepared_mailer_for(receiver.account).follow }

    include_examples 'localized subject', 'notification_mailer.follow.subject', name: 'bob'
    include_examples 'headers', 'follow', false

    it 'renders the headers' do
    it 'renders the subject' do
      expect(mail.subject).to eq('bob is now following you')
      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
    end

    it 'renders the body' do


@@ 49,10 70,10 @@ RSpec.describe NotificationMailer do
    let(:mail) { prepared_mailer_for(own_status.account).favourite }

    include_examples 'localized subject', 'notification_mailer.favourite.subject', name: 'bob'
    include_examples 'headers', 'favourite', true

    it 'renders the headers' do
    it 'renders the subject' do
      expect(mail.subject).to eq('bob favorited your post')
      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
    end

    it 'renders the body' do


@@ 67,10 88,10 @@ RSpec.describe NotificationMailer do
    let(:mail) { prepared_mailer_for(own_status.account).reblog }

    include_examples 'localized subject', 'notification_mailer.reblog.subject', name: 'bob'
    include_examples 'headers', 'reblog', true

    it 'renders the headers' do
    it 'renders the subject' do
      expect(mail.subject).to eq('bob boosted your post')
      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
    end

    it 'renders the body' do


@@ 85,10 106,10 @@ RSpec.describe NotificationMailer do
    let(:mail) { prepared_mailer_for(receiver.account).follow_request }

    include_examples 'localized subject', 'notification_mailer.follow_request.subject', name: 'bob'
    include_examples 'headers', 'follow_request', false

    it 'renders the headers' do
    it 'renders the subject' do
      expect(mail.subject).to eq('Pending follower: bob')
      expect(mail[:to].value).to eq("#{receiver.account.username} <#{receiver.email}>")
    end

    it 'renders the body' do

D spec/models/account_alias_spec.rb => spec/models/account_alias_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AccountAlias do
end

D spec/models/account_deletion_request_spec.rb => spec/models/account_deletion_request_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AccountDeletionRequest do
end

D spec/models/account_moderation_note_spec.rb => spec/models/account_moderation_note_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AccountModerationNote do
end

D spec/models/announcement_mute_spec.rb => spec/models/announcement_mute_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AnnouncementMute do
end

D spec/models/announcement_reaction_spec.rb => spec/models/announcement_reaction_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe AnnouncementReaction do
end

D spec/models/announcement_spec.rb => spec/models/announcement_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Announcement do
end

D spec/models/backup_spec.rb => spec/models/backup_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Backup do
end

D spec/models/conversation_mute_spec.rb => spec/models/conversation_mute_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ConversationMute do
end

D spec/models/custom_filter_keyword_spec.rb => spec/models/custom_filter_keyword_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe CustomFilterKeyword do
end

D spec/models/custom_filter_spec.rb => spec/models/custom_filter_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe CustomFilter do
end

D spec/models/device_spec.rb => spec/models/device_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Device do
end

D spec/models/encrypted_message_spec.rb => spec/models/encrypted_message_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe EncryptedMessage do
end

D spec/models/featured_tag_spec.rb => spec/models/featured_tag_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe FeaturedTag do
end

D spec/models/follow_recommendation_suppression_spec.rb => spec/models/follow_recommendation_suppression_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe FollowRecommendationSuppression do
end

D spec/models/list_account_spec.rb => spec/models/list_account_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ListAccount do
end

D spec/models/list_spec.rb => spec/models/list_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe List do
end

D spec/models/login_activity_spec.rb => spec/models/login_activity_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe LoginActivity do
end

M spec/models/media_attachment_spec.rb => spec/models/media_attachment_spec.rb +82 -33
@@ 84,7 84,87 @@ RSpec.describe MediaAttachment, paperclip_processing: true do
    end
  end

  describe 'animated gif conversion' do
  shared_examples 'static 600x400 image' do |content_type, extension|
    after do
      media.destroy
    end

    it 'saves media attachment' do
      expect(media.persisted?).to be true
      expect(media.file).to_not be_nil
    end

    it 'completes processing' do
      expect(media.processing_complete?).to be true
    end

    it 'sets type' do
      expect(media.type).to eq 'image'
    end

    it 'sets content type' do
      expect(media.file_content_type).to eq content_type
    end

    it 'sets file extension' do
      expect(media.file_file_name).to end_with extension
    end

    it 'strips original file name' do
      expect(media.file_file_name).to_not start_with '600x400'
    end

    it 'sets meta for original' do
      expect(media.file.meta['original']['width']).to eq 600
      expect(media.file.meta['original']['height']).to eq 400
      expect(media.file.meta['original']['aspect']).to eq 1.5
    end

    it 'sets meta for thumbnail' do
      expect(media.file.meta['small']['width']).to eq 588
      expect(media.file.meta['small']['height']).to eq 392
      expect(media.file.meta['small']['aspect']).to eq 1.5
    end
  end

  describe 'jpeg' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('600x400.jpeg')) }

    it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  end

  describe 'png' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('600x400.png')) }

    it_behaves_like 'static 600x400 image', 'image/png', '.png'
  end

  describe 'webp' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('600x400.webp')) }

    it_behaves_like 'static 600x400 image', 'image/webp', '.webp'
  end

  describe 'avif' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('600x400.avif')) }

    it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  end

  describe 'heic' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('600x400.heic')) }

    it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  end

  describe 'base64-encoded image' do
    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('600x400.jpeg').read)}" }
    let(:media) { described_class.create(account: Fabricate(:account), file: base64_attachment) }

    it_behaves_like 'static 600x400 image', 'image/jpeg', '.jpeg'
  end

  describe 'animated gif' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('avatar.gif')) }

    it 'sets type to gifv' do


@@ 101,7 181,7 @@ RSpec.describe MediaAttachment, paperclip_processing: true do
    end
  end

  describe 'non-animated gif non-conversion' do
  describe 'static gif' do
    fixtures = [
      { filename: 'attachment.gif', width: 600, height: 400, aspect: 1.5 },
      { filename: 'mini-static.gif', width: 32, height: 32, aspect: 1.0 },


@@ 172,37 252,6 @@ RSpec.describe MediaAttachment, paperclip_processing: true do
    end
  end

  describe 'jpeg' do
    let(:media) { described_class.create(account: Fabricate(:account), file: attachment_fixture('attachment.jpg')) }

    it 'sets meta for different style' do
      expect(media.file.meta['original']['width']).to eq 600
      expect(media.file.meta['original']['height']).to eq 400
      expect(media.file.meta['original']['aspect']).to eq 1.5
      expect(media.file.meta['small']['width']).to eq 588
      expect(media.file.meta['small']['height']).to eq 392
      expect(media.file.meta['small']['aspect']).to eq 1.5
    end

    it 'gives the file a random name' do
      expect(media.file_file_name).to_not eq 'attachment.jpg'
    end
  end

  describe 'base64-encoded jpeg' do
    let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
    let(:media) { described_class.create(account: Fabricate(:account), file: base64_attachment) }

    it 'saves media attachment' do
      expect(media.persisted?).to be true
      expect(media.file).to_not be_nil
    end

    it 'gives the file a file name' do
      expect(media.file_file_name).to_not be_blank
    end
  end

  it 'is invalid without file' do
    media = described_class.new(account: Fabricate(:account))
    expect(media.valid?).to be false

D spec/models/mute_spec.rb => spec/models/mute_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Mute do
end

D spec/models/preview_card_spec.rb => spec/models/preview_card_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PreviewCard do
end

D spec/models/preview_card_trend_spec.rb => spec/models/preview_card_trend_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe PreviewCardTrend do
end

D spec/models/relay_spec.rb => spec/models/relay_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Relay do
end

D spec/models/scheduled_status_spec.rb => spec/models/scheduled_status_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe ScheduledStatus do
end

D spec/models/status_stat_spec.rb => spec/models/status_stat_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe StatusStat do
end

D spec/models/status_trend_spec.rb => spec/models/status_trend_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe StatusTrend do
end

D spec/models/system_key_spec.rb => spec/models/system_key_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe SystemKey do
end

D spec/models/tag_follow_spec.rb => spec/models/tag_follow_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe TagFollow do
end

D spec/models/unavailable_domain_spec.rb => spec/models/unavailable_domain_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UnavailableDomain do
end

D spec/models/user_invite_request_spec.rb => spec/models/user_invite_request_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UserInviteRequest do
end

D spec/models/web/setting_spec.rb => spec/models/web/setting_spec.rb +0 -6
@@ 1,6 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe Web::Setting do
end

M spec/rails_helper.rb => spec/rails_helper.rb +53 -3
@@ 1,6 1,14 @@
# frozen_string_literal: true

ENV['RAILS_ENV'] ||= 'test'

# This needs to be defined before Rails is initialized
RUN_SYSTEM_SPECS = ENV.fetch('RUN_SYSTEM_SPECS', false)

if RUN_SYSTEM_SPECS
  STREAMING_PORT = ENV.fetch('TEST_STREAMING_PORT', '4020')
  ENV['STREAMING_API_BASE_URL'] = "http://localhost:#{STREAMING_PORT}"
end
require File.expand_path('../config/environment', __dir__)

abort('The Rails environment is running in production mode!') if Rails.env.production?


@@ 15,10 23,14 @@ require 'chewy/rspec'
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f }

ActiveRecord::Migration.maintain_test_schema!
WebMock.disable_net_connect!(allow: Chewy.settings[:host])
WebMock.disable_net_connect!(allow: Chewy.settings[:host], allow_localhost: RUN_SYSTEM_SPECS)
Sidekiq::Testing.inline!
Sidekiq.logger = nil

# System tests config
DatabaseCleaner.strategy = [:deletion]
streaming_server_manager = StreamingServerManager.new

Devise::Test::ControllerHelpers.module_eval do
  alias_method :original_sign_in, :sign_in



@@ 56,6 68,8 @@ module SignedRequestHelpers
end

RSpec.configure do |config|
  # This is set before running spec:system, see lib/tasks/tests.rake
  config.filter_run_excluding type: :system unless RUN_SYSTEM_SPECS
  config.fixture_path = Rails.root.join('spec', 'fixtures')
  config.use_transactional_fixtures = true
  config.order = 'random'


@@ 83,8 97,7 @@ RSpec.configure do |config|
  end

  config.before :each, type: :feature do
    https = ENV['LOCAL_HTTPS'] == 'true'
    Capybara.app_host = "http#{https ? 's' : ''}://#{ENV.fetch('LOCAL_DOMAIN')}"
    Capybara.current_driver = :rack_test
  end

  config.before :each, type: :controller do


@@ 95,6 108,35 @@ RSpec.configure do |config|
    stub_jsonld_contexts!
  end

  config.before :suite do
    if RUN_SYSTEM_SPECS
      Webpacker.compile
      streaming_server_manager.start(port: STREAMING_PORT)
    end
  end

  config.after :suite do
    streaming_server_manager.stop
  end

  config.around :each, type: :system do |example|
    # driven_by :selenium, using: :chrome, screen_size: [1600, 1200]
    driven_by :selenium, using: :headless_chrome, screen_size: [1600, 1200]

    # The streaming server needs access to the database
    # but with use_transactional_tests every transaction
    # is rolled-back, so the streaming server never sees the data
    # So we disable this feature for system tests, and use DatabaseCleaner to clean
    # the database tables between each test
    self.use_transactional_tests = false

    DatabaseCleaner.cleaning do
      example.run
    end

    self.use_transactional_tests = true
  end

  config.before(:each) do |example|
    unless example.metadata[:paperclip_processing]
      allow_any_instance_of(Paperclip::Attachment).to receive(:post_process).and_return(true) # rubocop:disable RSpec/AnyInstance


@@ 105,6 147,14 @@ RSpec.configure do |config|
    Rails.cache.clear
    redis.del(redis.keys)
  end

  # Assign types based on dir name for non-inferred types
  config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
    unless metadata.key?(:type)
      match = metadata[:location].match(%r{/spec/([^/]+)/})
      metadata[:type] = match[1].singularize.to_sym
    end
  end
end

RSpec::Sidekiq.configure do |config|

M spec/requests/cache_spec.rb => spec/requests/cache_spec.rb +3 -3
@@ 508,12 508,12 @@ describe 'Caching behavior' do
  context 'when enabling LIMITED_FEDERATION_MODE mode' do
    around do |example|
      ClimateControl.modify LIMITED_FEDERATION_MODE: 'true' do
        old_whitelist_mode = Rails.configuration.x.whitelist_mode
        Rails.configuration.x.whitelist_mode = true
        old_limited_federation_mode = Rails.configuration.x.limited_federation_mode
        Rails.configuration.x.limited_federation_mode = true

        example.run

        Rails.configuration.x.whitelist_mode = old_whitelist_mode
        Rails.configuration.x.limited_federation_mode = old_limited_federation_mode
      end
    end


A spec/requests/mail_subscriptions_spec.rb => spec/requests/mail_subscriptions_spec.rb +103 -0
@@ 0,0 1,103 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe 'MailSubscriptionsController' do
  let(:user) { Fabricate(:user) }
  let(:token) { user.to_sgid(for: 'unsubscribe').to_s }
  let(:type) { 'follow' }

  shared_examples 'not found with invalid token' do
    context 'with invalid token' do
      let(:token) { 'invalid-token' }

      it 'returns http not found' do
        expect(response).to have_http_status(404)
      end
    end
  end

  shared_examples 'not found with invalid type' do
    context 'with invalid type' do
      let(:type) { 'invalid_type' }

      it 'returns http not found' do
        expect(response).to have_http_status(404)
      end
    end
  end

  describe 'on the unsubscribe confirmation page' do
    before do
      get unsubscribe_url(token: token, type: type)
    end

    it_behaves_like 'not found with invalid token'
    it_behaves_like 'not found with invalid type'

    it 'shows unsubscribe form' do
      expect(response).to have_http_status(200)

      expect(response.body).to include(
        I18n.t('mail_subscriptions.unsubscribe.action')
      )
      expect(response.body).to include(user.email)
    end
  end

  describe 'submitting the unsubscribe confirmation page' do
    before do
      user.settings.update('notification_emails.follow': true)
      user.save!

      post unsubscribe_url, params: { token: token, type: type }
    end

    it_behaves_like 'not found with invalid token'
    it_behaves_like 'not found with invalid type'

    it 'shows confirmation page' do
      expect(response).to have_http_status(200)

      expect(response.body).to include(
        I18n.t('mail_subscriptions.unsubscribe.complete')
      )
      expect(response.body).to include(user.email)
    end

    it 'updates notification settings' do
      user.reload
      expect(user.settings['notification_emails.follow']).to be false
    end
  end

  describe 'unsubscribing with List-Unsubscribe-Post' do
    around do |example|
      old = ActionController::Base.allow_forgery_protection
      ActionController::Base.allow_forgery_protection = true

      example.run

      ActionController::Base.allow_forgery_protection = old
    end

    before do
      user.settings.update('notification_emails.follow': true)
      user.save!

      post unsubscribe_url(token: token, type: type), params: { 'List-Unsubscribe' => 'One-Click' }
    end

    it_behaves_like 'not found with invalid token'
    it_behaves_like 'not found with invalid type'

    it 'return http success' do
      expect(response).to have_http_status(200)
    end

    it 'updates notification settings' do
      user.reload
      expect(user.settings['notification_emails.follow']).to be false
    end
  end
end

M spec/services/fetch_link_card_service_spec.rb => spec/services/fetch_link_card_service_spec.rb +173 -36
@@ 5,96 5,233 @@ require 'rails_helper'
RSpec.describe FetchLinkCardService, type: :service do
  subject { described_class.new }

  let(:html) { '<!doctype html><title>Hello world</title>' }
  let(:oembed_cache) { nil }

  before do
    stub_request(:get, 'http://example.xn--fiqs8s/').to_return(request_fixture('idn.txt'))
    stub_request(:get, 'http://example.com/html').to_return(headers: { 'Content-Type' => 'text/html' }, body: html)
    stub_request(:get, 'http://example.com/not-found').to_return(status: 404, headers: { 'Content-Type' => 'text/html' }, body: html)
    stub_request(:get, 'http://example.com/text').to_return(status: 404, headers: { 'Content-Type' => 'text/plain' }, body: 'Hello')
    stub_request(:get, 'http://example.com/redirect').to_return(status: 302, headers: { 'Location' => 'http://example.com/html' })
    stub_request(:get, 'http://example.com/redirect-to-404').to_return(status: 302, headers: { 'Location' => 'http://example.com/not-found' })
    stub_request(:get, 'http://example.com/oembed?url=http://example.com/html').to_return(headers: { 'Content-Type' => 'application/json' }, body: '{ "version": "1.0", "type": "link", "title": "oEmbed title" }')
    stub_request(:get, 'http://example.com/oembed?format=json&url=http://example.com/html').to_return(headers: { 'Content-Type' => 'application/json' }, body: '{ "version": "1.0", "type": "link", "title": "oEmbed title" }')

    stub_request(:get, 'http://example.xn--fiqs8s')
    stub_request(:get, 'http://example.com/日本語')
    stub_request(:get, 'http://example.com/test?data=file.gpx%5E1')
    stub_request(:get, 'http://example.com/test-')

    stub_request(:get, 'http://example.com/sjis').to_return(request_fixture('sjis.txt'))
    stub_request(:get, 'http://example.com/sjis_with_wrong_charset').to_return(request_fixture('sjis_with_wrong_charset.txt'))
    stub_request(:get, 'http://example.com/koi8-r').to_return(request_fixture('koi8-r.txt'))
    stub_request(:get, 'http://example.com/日本語').to_return(request_fixture('sjis.txt'))
    stub_request(:get, 'https://github.com/qbi/WannaCry').to_return(status: 404)
    stub_request(:get, 'http://example.com/test?data=file.gpx%5E1').to_return(status: 200)
    stub_request(:get, 'http://example.com/test-').to_return(request_fixture('idn.txt'))
    stub_request(:get, 'http://example.com/windows-1251').to_return(request_fixture('windows-1251.txt'))

    Rails.cache.write('oembed_endpoint:example.com', oembed_cache) if oembed_cache

    subject.call(status)
  end

  context 'with a local status' do
    context 'with an IDN url' do
    context 'with URL of a regular HTML page' do
      let(:status) { Fabricate(:status, text: 'http://example.com/html') }

      it 'creates preview card' do
        expect(status.preview_card).to_not be_nil
        expect(status.preview_card.url).to eq 'http://example.com/html'
        expect(status.preview_card.title).to eq 'Hello world'
      end
    end

    context 'with URL of a page with no title' do
      let(:status) { Fabricate(:status, text: 'http://example.com/html') }
      let(:html) { '<!doctype html><title></title>' }

      it 'does not create a preview card' do
        expect(status.preview_card).to be_nil
      end
    end

    context 'with a URL of a plain-text page' do
      let(:status) { Fabricate(:status, text: 'http://example.com/text') }

      it 'does not create a preview card' do
        expect(status.preview_card).to be_nil
      end
    end

    context 'with multiple URLs' do
      let(:status) { Fabricate(:status, text: 'ftp://example.com http://example.com/html http://example.com/text') }

      it 'fetches the first valid URL' do
        expect(a_request(:get, 'http://example.com/html')).to have_been_made
      end

      it 'does not fetch the second valid URL' do
        expect(a_request(:get, 'http://example.com/text/')).to_not have_been_made
      end
    end

    context 'with a redirect URL' do
      let(:status) { Fabricate(:status, text: 'http://example.com/redirect') }

      it 'follows redirect' do
        expect(a_request(:get, 'http://example.com/redirect')).to have_been_made.once
        expect(a_request(:get, 'http://example.com/html')).to have_been_made.once
      end

      it 'creates preview card' do
        expect(status.preview_card).to_not be_nil
        expect(status.preview_card.url).to eq 'http://example.com/html'
        expect(status.preview_card.title).to eq 'Hello world'
      end
    end

    context 'with a broken redirect URL' do
      let(:status) { Fabricate(:status, text: 'http://example.com/redirect-to-404') }

      it 'follows redirect' do
        expect(a_request(:get, 'http://example.com/redirect-to-404')).to have_been_made.once
        expect(a_request(:get, 'http://example.com/not-found')).to have_been_made.once
      end

      it 'does not create a preview card' do
        expect(status.preview_card).to be_nil
      end
    end

    context 'with a 404 URL' do
      let(:status) { Fabricate(:status, text: 'http://example.com/not-found') }

      it 'does not create a preview card' do
        expect(status.preview_card).to be_nil
      end
    end

    context 'with an IDN URL' do
      let(:status) { Fabricate(:status, text: 'Check out http://example.中国') }

      it 'works with IDN URLs' do
        expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.at_least_once
      it 'fetches the URL' do
        expect(a_request(:get, 'http://example.xn--fiqs8s/')).to have_been_made.once
      end
    end

    context 'with an SJIS url' do
    context 'with a URL of a page in Shift JIS encoding' do
      let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis') }

      it 'works with SJIS' do
        expect(a_request(:get, 'http://example.com/sjis')).to have_been_made.at_least_once
      it 'decodes the HTML' do
        expect(status.preview_cards.first.title).to eq('SJISのページ')
      end
    end

    context 'with invalid SJIS url' do
    context 'with a URL of a page in Shift JIS encoding labeled as UTF-8' do
      let(:status) { Fabricate(:status, text: 'Check out http://example.com/sjis_with_wrong_charset') }

      it 'works with SJIS even with wrong charset header' do
        expect(a_request(:get, 'http://example.com/sjis_with_wrong_charset')).to have_been_made.at_least_once
      it 'decodes the HTML despite the wrong charset header' do
        expect(status.preview_cards.first.title).to eq('SJISのページ')
      end
    end

    context 'with an koi8-r url' do
    context 'with a URL of a page in KOI8-R encoding' do
      let(:status) { Fabricate(:status, text: 'Check out http://example.com/koi8-r') }

      it 'works with koi8-r' do
        expect(a_request(:get, 'http://example.com/koi8-r')).to have_been_made.at_least_once
      it 'decodes the HTML' do
        expect(status.preview_cards.first.title).to eq('Московя начинаетъ только въ XVI ст. привлекать внимане иностранцевъ.')
      end
    end

    context 'with a windows-1251 url' do
    context 'with a URL of a page in Windows-1251 encoding' do
      let(:status) { Fabricate(:status, text: 'Check out http://example.com/windows-1251') }

      it 'works with windows-1251' do
        expect(a_request(:get, 'http://example.com/windows-1251')).to have_been_made.at_least_once
      it 'decodes the HTML' do
        expect(status.preview_cards.first.title).to eq('сэмпл текст')
      end
    end

    context 'with a japanese path url' do
    context 'with a Japanese path URL' do
      let(:status) { Fabricate(:status, text: 'テストhttp://example.com/日本語') }

      it 'works with Japanese path string' do
        expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.at_least_once
        expect(status.preview_cards.first.title).to eq('SJISのページ')
      it 'fetches the URL' do
        expect(a_request(:get, 'http://example.com/日本語')).to have_been_made.once
      end
    end

    context 'with a hyphen-suffixed url' do
    context 'with a hyphen-suffixed URL' do
      let(:status) { Fabricate(:status, text: 'test http://example.com/test-') }

      it 'works with a URL ending with a hyphen' do
        expect(a_request(:get, 'http://example.com/test-')).to have_been_made.at_least_once
      it 'fetches the URL' do
        expect(a_request(:get, 'http://example.com/test-')).to have_been_made.once
      end
    end

    context 'with an isolated url' do
    context 'with a caret-suffixed URL' do
      let(:status) { Fabricate(:status, text: 'test http://example.com/test?data=file.gpx^1') }

      it 'fetches the URL' do
        expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
      end

      it 'does not strip the caret before fetching' do
        expect(a_request(:get, 'http://example.com/test?data=file.gpx')).to_not have_been_made
      end
    end

    context 'with a non-isolated URL' do
      let(:status) { Fabricate(:status, text: 'testhttp://example.com/sjis') }

      it 'does not fetch URLs with not isolated from their surroundings' do
      it 'does not fetch URLs not isolated from their surroundings' do
        expect(a_request(:get, 'http://example.com/sjis')).to_not have_been_made
      end
    end

    context 'with a url that has a caret' do
      let(:status) { Fabricate(:status, text: 'test http://example.com/test?data=file.gpx^1') }
    context 'with a URL of a page with oEmbed support' do
      let(:html) { '<!doctype html><title>Hello world</title><link rel="alternate" type="application/json+oembed" href="http://example.com/oembed?url=http://example.com/html">' }
      let(:status) { Fabricate(:status, text: 'http://example.com/html') }

      it 'does fetch URLs with a caret in search params' do
        expect(a_request(:get, 'http://example.com/test?data=file.gpx')).to_not have_been_made
        expect(a_request(:get, 'http://example.com/test?data=file.gpx%5E1')).to have_been_made.once
      it 'fetches the oEmbed URL' do
        expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to have_been_made.once
      end

      it 'creates preview card' do
        expect(status.preview_card).to_not be_nil
        expect(status.preview_card.url).to eq 'http://example.com/html'
        expect(status.preview_card.title).to eq 'oEmbed title'
      end

      context 'when oEmbed endpoint cache populated' do
        let(:oembed_cache) { { endpoint: 'http://example.com/oembed?format=json&url={url}', format: :json } }

        it 'uses the cached oEmbed response' do
          expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to_not have_been_made
          expect(a_request(:get, 'http://example.com/oembed?format=json&url=http://example.com/html')).to have_been_made
        end

        it 'creates preview card' do
          expect(status.preview_card).to_not be_nil
          expect(status.preview_card.url).to eq 'http://example.com/html'
          expect(status.preview_card.title).to eq 'oEmbed title'
        end
      end

      # If the original HTML URL for whatever reason (e.g. DOS protection) redirects to
      # an error page, we can still use the cached oEmbed but should not use the
      # redirect URL on the card.
      context 'when oEmbed endpoint cache populated but page returns 404' do
        let(:status) { Fabricate(:status, text: 'http://example.com/redirect-to-404') }
        let(:oembed_cache) { { endpoint: 'http://example.com/oembed?url=http://example.com/html', format: :json } }

        it 'uses the cached oEmbed response' do
          expect(a_request(:get, 'http://example.com/oembed?url=http://example.com/html')).to have_been_made
        end

        it 'creates preview card' do
          expect(status.preview_card).to_not be_nil
          expect(status.preview_card.title).to eq 'oEmbed title'
        end

        it 'uses the original URL' do
          expect(status.preview_card&.url).to eq 'http://example.com/redirect-to-404'
        end
      end
    end
  end


@@ 104,13 241,13 @@ RSpec.describe FetchLinkCardService, type: :service do
      Fabricate(:status, account: Fabricate(:account, domain: 'example.com'), text: <<-TEXT)
      Habt ihr ein paar gute Links zu <a>foo</a>
      #<span class="tag"><a href="https://quitter.se/tag/wannacry" target="_blank" rel="tag noopener noreferrer" title="https://quitter.se/tag/wannacry">Wannacry</a></span> herumfliegen?
      Ich will mal unter <br> <a href="https://github.com/qbi/WannaCry" target="_blank" rel="noopener noreferrer" title="https://github.com/qbi/WannaCry">https://github.com/qbi/WannaCry</a> was sammeln. !
      Ich will mal unter <br> <a href="http://example.com/not-found" target="_blank" rel="noopener noreferrer" title="http://example.com/not-found">http://example.com/not-found</a> was sammeln. !
      <a href="http://sn.jonkman.ca/group/416/id" target="_blank" rel="noopener noreferrer" title="http://sn.jonkman.ca/group/416/id">security</a>&nbsp;
      TEXT
    end

    it 'parses out URLs' do
      expect(a_request(:get, 'https://github.com/qbi/WannaCry')).to have_been_made.at_least_once
      expect(a_request(:get, 'http://example.com/not-found')).to have_been_made.once
    end

    it 'ignores URLs to hashtags' do

M spec/services/unallow_domain_service_spec.rb => spec/services/unallow_domain_service_spec.rb +2 -2
@@ 14,7 14,7 @@ RSpec.describe UnallowDomainService, type: :service do

  context 'with limited federation mode' do
    before do
      allow(Rails.configuration.x).to receive(:whitelist_mode).and_return(true)
      allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(true)
    end

    describe '#call' do


@@ 40,7 40,7 @@ RSpec.describe UnallowDomainService, type: :service do

  context 'without limited federation mode' do
    before do
      allow(Rails.configuration.x).to receive(:whitelist_mode).and_return(false)
      allow(Rails.configuration.x).to receive(:limited_federation_mode).and_return(false)
    end

    describe '#call' do

D spec/services/unmute_service_spec.rb => spec/services/unmute_service_spec.rb +0 -7
@@ 1,7 0,0 @@
# frozen_string_literal: true

require 'rails_helper'

RSpec.describe UnmuteService, type: :service do
  subject { described_class.new }
end

M spec/spec_helper.rb => spec/spec_helper.rb +77 -0
@@ 52,3 52,80 @@ def expect_push_bulk_to_match(klass, matcher)
    'args' => matcher,
  }))
end

class StreamingServerManager
  @running_thread = nil

  def initialize
    at_exit { stop }
  end

  def start(port: 4020)
    return if @running_thread

    queue = Queue.new

    @queue = queue

    @running_thread = Thread.new do
      Open3.popen2e(
        {
          'REDIS_NAMESPACE' => ENV.fetch('REDIS_NAMESPACE'),
          'DB_NAME' => "#{ENV.fetch('DB_NAME', 'mastodon')}_test#{ENV.fetch('TEST_ENV_NUMBER', '')}",
          'RAILS_ENV' => ENV.fetch('RAILS_ENV', 'test'),
          'NODE_ENV' => ENV.fetch('STREAMING_NODE_ENV', 'development'),
          'PORT' => port.to_s,
        },
        'node index.js', # must not call yarn here, otherwise it will fail because yarn does not send signals to its child process
        chdir: Rails.root.join('streaming')
      ) do |_stdin, stdout_err, process_thread|
        status = :starting

        # Spawn a thread to listen on streaming server output
        output_thread = Thread.new do
          stdout_err.each_line do |line|
            Rails.logger.info "Streaming server: #{line}"

            if status == :starting && line.match('Streaming API now listening on')
              status = :started
              @queue.enq 'started'
            end
          end
        end

        # And another thread to listen on commands from the main thread
        loop do
          msg = queue.pop

          case msg
          when 'stop'
            # we need to properly stop the reading thread
            output_thread.kill

            # Then stop the node process
            Process.kill('KILL', process_thread.pid)

            # And we stop ourselves
            @running_thread.kill
          end
        end
      end
    end

    # wait for 10 seconds for the streaming server to start
    Timeout.timeout(10) do
      loop do
        break if @queue.pop == 'started'
      end
    end
  end

  def stop
    return unless @running_thread

    @queue.enq 'stop'

    # Wait for the thread to end
    @running_thread.join
  end
end

M spec/support/stories/profile_stories.rb => spec/support/stories/profile_stories.rb +6 -0
@@ 9,6 9,8 @@ module ProfileStories
      email: email, password: password, confirmed_at: confirmed_at,
      account: Fabricate(:account, username: 'bob')
    )

    Web::Setting.where(user: bob).first_or_initialize(user: bob).update!(data: { introductionVersion: 201812160442020 }) if finished_onboarding # rubocop:disable Style/NumericLiterals
  end

  def as_a_logged_in_user


@@ 42,4 44,8 @@ module ProfileStories
  def password
    @password ||= 'password'
  end

  def finished_onboarding
    @finished_onboarding || false
  end
end

A spec/system/new_statuses_spec.rb => spec/system/new_statuses_spec.rb +45 -0
@@ 0,0 1,45 @@
# frozen_string_literal: true

require 'rails_helper'

describe 'NewStatuses' do
  include ProfileStories

  subject { page }

  let(:email)               { 'test@example.com' }
  let(:password)            { 'password' }
  let(:confirmed_at)        { Time.zone.now }
  let(:finished_onboarding) { true }

  before do
    as_a_logged_in_user
    visit root_path
  end

  it 'can be posted' do
    expect(subject).to have_css('div.app-holder')

    status_text = 'This is a new status!'

    within('.compose-form') do
      fill_in "What's on your mind?", with: status_text
      click_on 'Publish!'
    end

    expect(subject).to have_selector('.status__content__text', text: status_text)
  end

  it 'can be posted again' do
    expect(subject).to have_css('div.app-holder')

    status_text = 'This is a second status!'

    within('.compose-form') do
      fill_in "What's on your mind?", with: status_text
      click_on 'Publish!'
    end

    expect(subject).to have_selector('.status__content__text', text: status_text)
  end
end

A spec/validators/language_validator_spec.rb => spec/validators/language_validator_spec.rb +60 -0
@@ 0,0 1,60 @@
# frozen_string_literal: true

require 'rails_helper'

describe LanguageValidator do
  let(:record_class) do
    Class.new do
      include ActiveModel::Validations
      attr_accessor :locale

      validates :locale, language: true
    end
  end
  let(:record) { record_class.new }

  describe '#validate_each' do
    context 'with a nil value' do
      it 'does not add errors' do
        record.locale = nil

        expect(record).to be_valid
        expect(record.errors).to be_empty
      end
    end

    context 'with an array of values' do
      it 'does not add errors with array of existing locales' do
        record.locale = %w(en fr)

        expect(record).to be_valid
        expect(record.errors).to be_empty
      end

      it 'adds errors with array having some non-existing locales' do
        record.locale = %w(en fr missing)

        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:locale)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end

    context 'with a locale string' do
      it 'does not add errors when string is an existing locale' do
        record.locale = 'en'

        expect(record).to be_valid
        expect(record.errors).to be_empty
      end

      it 'adds errors when string is non-existing locale' do
        record.locale = 'missing'

        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:locale)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end
  end
end

M spec/validators/url_validator_spec.rb => spec/validators/url_validator_spec.rb +49 -17
@@ 2,32 2,64 @@

require 'rails_helper'

RSpec.describe URLValidator, type: :validator do
describe URLValidator do
  let(:record_class) do
    Class.new do
      include ActiveModel::Validations
      attr_accessor :profile

      validates :profile, url: true
    end
  end
  let(:record) { record_class.new }

  describe '#validate_each' do
    before do
      allow(validator).to receive(:compliant?).with(value) { compliant }
      validator.validate_each(record, attribute, value)
    context 'with a nil value' do
      it 'adds errors' do
        record.profile = nil

        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:profile)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end

    let(:validator) { described_class.new(attributes: [attribute]) }
    let(:record)    { instance_double(Webhook, errors: errors) }
    let(:errors)    { instance_double(ActiveModel::Errors, add: nil) }
    let(:value)     { '' }
    let(:attribute) { :foo }
    context 'with an invalid url scheme' do
      it 'adds errors' do
        record.profile = 'ftp://example.com/page'

        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:profile)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end

    context 'without a hostname' do
      it 'adds errors' do
        record.profile = 'https:///page'

        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:profile)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end

    context 'when not compliant?' do
      let(:compliant) { false }
    context 'with an unparseable value' do
      it 'adds errors' do
        record.profile = 'https://host:port/page' # non-numeric port string causes invalid uri error

      it 'calls errors.add' do
        expect(errors).to have_received(:add).with(attribute, :invalid)
        expect(record).to_not be_valid
        expect(record.errors.first.attribute).to eq(:profile)
        expect(record.errors.first.type).to eq(:invalid)
      end
    end

    context 'when compliant?' do
      let(:compliant) { true }
    context 'with a valid url' do
      it 'does not add errors' do
        record.profile = 'https://example.com/page'

      it 'not calls errors.add' do
        expect(errors).to_not have_received(:add).with(attribute, any_args)
        expect(record).to be_valid
        expect(record.errors).to be_empty
      end
    end
  end

M yarn.lock => yarn.lock +414 -10
@@ 786,6 786,13 @@
  dependencies:
    "@babel/helper-plugin-utils" "^7.22.5"

"@babel/plugin-transform-react-constant-elements@^7.12.1":
  version "7.22.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.5.tgz#6dfa7c1c37f7d7279e417ceddf5a04abb8bb9c29"
  integrity sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA==
  dependencies:
    "@babel/helper-plugin-utils" "^7.22.5"

"@babel/plugin-transform-react-display-name@^7.22.5":
  version "7.22.5"
  resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz#3c4326f9fce31c7968d6cb9debcaf32d9e279a2b"


@@ 931,7 938,7 @@
    "@babel/helper-create-regexp-features-plugin" "^7.22.5"
    "@babel/helper-plugin-utils" "^7.22.5"

"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.22.4":
"@babel/preset-env@^7.11.0", "@babel/preset-env@^7.12.1", "@babel/preset-env@^7.22.4":
  version "7.22.9"
  resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.22.9.tgz#57f17108eb5dfd4c5c25a44c1977eba1df310ac7"
  integrity sha512-wNi5H/Emkhll/bqPjsjQorSykrlfY5OWakd6AulLvMEytpKasMVUpVy8RL4qBIBs5Ac6/5i0/Rv0b/Fg6Eag/g==


@@ 1028,7 1035,7 @@
    "@babel/types" "^7.4.4"
    esutils "^2.0.2"

"@babel/preset-react@^7.22.3":
"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.22.3":
  version "7.22.5"
  resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.22.5.tgz#c4d6058fbf80bccad02dd8c313a9aaa67e3c3dd6"
  integrity sha512-M+Is3WikOpEJHgR385HbuCITPTaPRaNkibTEa9oiofmJvIsrceb4yp9RL9Kb+TE8LznmeyZqpP+Lopwcx59xPQ==


@@ 1111,7 1118,7 @@
    debug "^4.1.0"
    globals "^11.1.0"

"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
"@babel/types@^7.0.0", "@babel/types@^7.0.0-beta.49", "@babel/types@^7.12.11", "@babel/types@^7.12.6", "@babel/types@^7.20.7", "@babel/types@^7.22.5", "@babel/types@^7.3.3", "@babel/types@^7.4.4":
  version "7.22.5"
  resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.22.5.tgz#cd93eeaab025880a3a47ec881f4b096a5b786fbe"
  integrity sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==


@@ 1687,6 1694,11 @@
    "@jridgewell/resolve-uri" "3.1.0"
    "@jridgewell/sourcemap-codec" "1.4.14"

"@material-design-icons/svg@^0.14.10":
  version "0.14.10"
  resolved "https://registry.yarnpkg.com/@material-design-icons/svg/-/svg-0.14.10.tgz#25804b66d0740b0bf8d6841fa343dfdd60f22e82"
  integrity sha512-rXxfqj5Su8i51aG8s8QRIe7mX1gB+C/ZCroLu3JvIsO3+Vx6PcWP97HLwIl7AQH/jYIHQlKq0E6OMqU91u5fCg==

"@nicolo-ribaudo/semver-v6@^6.3.3":
  version "6.3.3"
  resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz#ea6d23ade78a325f7a52750aab1526b02b628c29"


@@ 1878,6 1890,109 @@
    magic-string "^0.25.0"
    string.prototype.matchall "^4.0.6"

"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
  version "5.4.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz#81ef61947bb268eb9d50523446f9c638fb355906"
  integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==

"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0":
  version "5.4.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz#6b2c770c95c874654fd5e1d5ef475b78a0a962ef"
  integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==

"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1":
  version "5.0.1"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz#25621a8915ed7ad70da6cea3d0a6dbc2ea933efd"
  integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==

"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1":
  version "5.0.1"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz#0b221fc57f9fcd10e91fe219e2cd0dd03145a897"
  integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==

"@svgr/babel-plugin-svg-dynamic-title@^5.4.0":
  version "5.4.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz#139b546dd0c3186b6e5db4fefc26cb0baea729d7"
  integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==

"@svgr/babel-plugin-svg-em-dimensions@^5.4.0":
  version "5.4.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz#6543f69526632a133ce5cabab965deeaea2234a0"
  integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==

"@svgr/babel-plugin-transform-react-native-svg@^5.4.0":
  version "5.4.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz#00bf9a7a73f1cad3948cdab1f8dfb774750f8c80"
  integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==

"@svgr/babel-plugin-transform-svg-component@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz#583a5e2a193e214da2f3afeb0b9e8d3250126b4a"
  integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==

"@svgr/babel-preset@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/babel-preset/-/babel-preset-5.5.0.tgz#8af54f3e0a8add7b1e2b0fcd5a882c55393df327"
  integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==
  dependencies:
    "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0"
    "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0"
    "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1"
    "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1"
    "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0"
    "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0"
    "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0"
    "@svgr/babel-plugin-transform-svg-component" "^5.5.0"

"@svgr/core@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/core/-/core-5.5.0.tgz#82e826b8715d71083120fe8f2492ec7d7874a579"
  integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==
  dependencies:
    "@svgr/plugin-jsx" "^5.5.0"
    camelcase "^6.2.0"
    cosmiconfig "^7.0.0"

"@svgr/hast-util-to-babel-ast@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz#5ee52a9c2533f73e63f8f22b779f93cd432a5461"
  integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==
  dependencies:
    "@babel/types" "^7.12.6"

"@svgr/plugin-jsx@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz#1aa8cd798a1db7173ac043466d7b52236b369000"
  integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==
  dependencies:
    "@babel/core" "^7.12.3"
    "@svgr/babel-preset" "^5.5.0"
    "@svgr/hast-util-to-babel-ast" "^5.5.0"
    svg-parser "^2.0.2"

"@svgr/plugin-svgo@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz#02da55d85320549324e201c7b2e53bf431fcc246"
  integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==
  dependencies:
    cosmiconfig "^7.0.0"
    deepmerge "^4.2.2"
    svgo "^1.2.2"

"@svgr/webpack@^5.5.0":
  version "5.5.0"
  resolved "https://registry.yarnpkg.com/@svgr/webpack/-/webpack-5.5.0.tgz#aae858ee579f5fa8ce6c3166ef56c6a1b381b640"
  integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==
  dependencies:
    "@babel/core" "^7.12.3"
    "@babel/plugin-transform-react-constant-elements" "^7.12.1"
    "@babel/preset-env" "^7.12.1"
    "@babel/preset-react" "^7.12.5"
    "@svgr/core" "^5.5.0"
    "@svgr/plugin-jsx" "^5.5.0"
    "@svgr/plugin-svgo" "^5.5.0"
    loader-utils "^2.0.0"

"@testing-library/dom@^9.0.0":
  version "9.3.1"
  resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.3.1.tgz#8094f560e9389fb973fe957af41bf766937a9ee9"


@@ 2216,6 2331,11 @@
  resolved "https://registry.yarnpkg.com/@types/punycode/-/punycode-2.1.0.tgz#89e4f3d09b3f92e87a80505af19be7e0c31d4e83"
  integrity sha512-PG5aLpW6PJOeV2fHRslP4IOMWn+G+Uq8CfnyJ+PDS8ndCbU+soO+fB3NKCKo0p/Jh2Y4aPaiQZsrOXFdzpcA6g==

"@types/q@^1.5.1":
  version "1.5.5"
  resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.5.tgz#75a2a8e7d8ab4b230414505d92335d1dcb53a6df"
  integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==

"@types/qs@*":
  version "6.9.7"
  resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"


@@ 3044,6 3164,17 @@ array.prototype.flatmap@^1.3.1:
    es-abstract "^1.20.4"
    es-shim-unscopables "^1.0.0"

array.prototype.reduce@^1.0.5:
  version "1.0.5"
  resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac"
  integrity sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==
  dependencies:
    call-bind "^1.0.2"
    define-properties "^1.1.4"
    es-abstract "^1.20.4"
    es-array-method-boxes-properly "^1.0.0"
    is-string "^1.0.7"

array.prototype.tosorted@^1.1.1:
  version "1.1.1"
  resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.1.tgz#ccf44738aa2b5ac56578ffda97c03fd3e23dd532"


@@ 3055,6 3186,18 @@ array.prototype.tosorted@^1.1.1:
    es-shim-unscopables "^1.0.0"
    get-intrinsic "^1.1.3"

arraybuffer.prototype.slice@^1.0.1:
  version "1.0.1"
  resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb"
  integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw==
  dependencies:
    array-buffer-byte-length "^1.0.0"
    call-bind "^1.0.2"
    define-properties "^1.2.0"
    get-intrinsic "^1.2.1"
    is-array-buffer "^3.0.2"
    is-shared-array-buffer "^1.0.2"

arrify@^1.0.1:
  version "1.0.1"
  resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"


@@ 3446,7 3589,7 @@ bonjour@^3.5.0:
    multicast-dns "^6.0.1"
    multicast-dns-service-types "^1.1.0"

boolbase@^1.0.0:
boolbase@^1.0.0, boolbase@~1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
  integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==


@@ 3762,7 3905,7 @@ chalk@5.2.0:
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3"
  integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA==

chalk@^2.0.0, chalk@^2.4.2:
chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
  version "2.4.2"
  resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
  integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==


@@ 3934,6 4077,15 @@ co@^4.6.0:
  resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
  integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==

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==
  dependencies:
    "@types/q" "^1.5.1"
    chalk "^2.4.1"
    q "^1.1.2"

cocoon-js-vanilla@^1.3.0:
  version "1.3.0"
  resolved "https://registry.yarnpkg.com/cocoon-js-vanilla/-/cocoon-js-vanilla-1.3.0.tgz#1e53663f5d314e5e9b315b63eaf8ae701df113c0"


@@ 4285,6 4437,21 @@ css-loader@^5.2.7:
    schema-utils "^3.0.0"
    semver "^7.3.5"

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.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 "^3.2.1"
    domutils "^1.7.0"
    nth-check "^1.0.2"

css-select@^5.1.0:
  version "5.1.0"
  resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6"


@@ 4296,6 4463,22 @@ css-select@^5.1.0:
    domutils "^3.0.1"
    nth-check "^2.0.1"

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 "2.0.4"
    source-map "^0.6.1"

css-tree@^1.1.2:
  version "1.1.3"
  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
  integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
  dependencies:
    mdn-data "2.0.14"
    source-map "^0.6.1"

css-tree@^2.2.1, css-tree@^2.3.1:
  version "2.3.1"
  resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20"


@@ 4312,6 4495,11 @@ css-tree@~2.2.0:
    mdn-data "2.0.28"
    source-map-js "^1.0.1"

css-what@^3.2.1:
  version "3.4.2"
  resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
  integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==

css-what@^6.1.0:
  version "6.1.0"
  resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"


@@ 4375,6 4563,13 @@ cssnano@^6.0.1:
    cssnano-preset-default "^6.0.1"
    lilconfig "^2.1.0"

csso@^4.0.2:
  version "4.2.0"
  resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
  integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
  dependencies:
    css-tree "^1.1.2"

csso@^5.0.5:
  version "5.0.5"
  resolved "https://registry.yarnpkg.com/csso/-/csso-5.0.5.tgz#f9b7fe6cc6ac0b7d90781bb16d5e9874303e2ca6"


@@ 4748,6 4943,14 @@ dom-helpers@^5.0.1, dom-helpers@^5.2.0:
    "@babel/runtime" "^7.8.7"
    csstype "^3.0.2"

dom-serializer@0:
  version "0.2.2"
  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
  integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==
  dependencies:
    domelementtype "^2.0.1"
    entities "^2.0.0"

dom-serializer@^2.0.0:
  version "2.0.0"
  resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53"


@@ 4762,7 4965,12 @@ domain-browser@^1.1.1:
  resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
  integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==

domelementtype@^2.3.0:
domelementtype@1:
  version "1.3.1"
  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
  integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==

domelementtype@^2.0.1, domelementtype@^2.3.0:
  version "2.3.0"
  resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
  integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==


@@ 4781,6 4989,14 @@ domhandler@^5.0.2, domhandler@^5.0.3:
  dependencies:
    domelementtype "^2.3.0"

domutils@^1.7.0:
  version "1.7.0"
  resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a"
  integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==
  dependencies:
    dom-serializer "0"
    domelementtype "1"

domutils@^3.0.1:
  version "3.1.0"
  resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e"


@@ 4908,6 5124,11 @@ enhanced-resolve@^5.12.0:
    graceful-fs "^4.2.4"
    tapable "^2.2.0"

entities@^2.0.0:
  version "2.2.0"
  resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55"
  integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==

entities@^4.2.0, entities@^4.4.0:
  version "4.5.0"
  resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"


@@ 4934,6 5155,51 @@ error-stack-parser@^2.0.6:
  dependencies:
    stackframe "^1.3.4"

es-abstract@^1.17.2, es-abstract@^1.21.2:
  version "1.22.1"
  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc"
  integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==
  dependencies:
    array-buffer-byte-length "^1.0.0"
    arraybuffer.prototype.slice "^1.0.1"
    available-typed-arrays "^1.0.5"
    call-bind "^1.0.2"
    es-set-tostringtag "^2.0.1"
    es-to-primitive "^1.2.1"
    function.prototype.name "^1.1.5"
    get-intrinsic "^1.2.1"
    get-symbol-description "^1.0.0"
    globalthis "^1.0.3"
    gopd "^1.0.1"
    has "^1.0.3"
    has-property-descriptors "^1.0.0"
    has-proto "^1.0.1"
    has-symbols "^1.0.3"
    internal-slot "^1.0.5"
    is-array-buffer "^3.0.2"
    is-callable "^1.2.7"
    is-negative-zero "^2.0.2"
    is-regex "^1.1.4"
    is-shared-array-buffer "^1.0.2"
    is-string "^1.0.7"
    is-typed-array "^1.1.10"
    is-weakref "^1.0.2"
    object-inspect "^1.12.3"
    object-keys "^1.1.1"
    object.assign "^4.1.4"
    regexp.prototype.flags "^1.5.0"
    safe-array-concat "^1.0.0"
    safe-regex-test "^1.0.0"
    string.prototype.trim "^1.2.7"
    string.prototype.trimend "^1.0.6"
    string.prototype.trimstart "^1.0.6"
    typed-array-buffer "^1.0.0"
    typed-array-byte-length "^1.0.0"
    typed-array-byte-offset "^1.0.0"
    typed-array-length "^1.0.4"
    unbox-primitive "^1.0.2"
    which-typed-array "^1.1.10"

es-abstract@^1.19.0, es-abstract@^1.20.4:
  version "1.21.2"
  resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.21.2.tgz#a56b9695322c8a185dc25975aa3b8ec31d0e7eff"


@@ 4974,6 5240,11 @@ es-abstract@^1.19.0, es-abstract@^1.20.4:
    unbox-primitive "^1.0.2"
    which-typed-array "^1.1.9"

es-array-method-boxes-properly@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e"
  integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==

es-get-iterator@^1.1.3:
  version "1.1.3"
  resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6"


@@ 5850,7 6121,7 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==

get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0:
get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1:
  version "1.2.1"
  resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82"
  integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==


@@ 6107,7 6378,7 @@ has-proto@^1.0.1:
  resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0"
  integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==

has-symbols@^1.0.2, has-symbols@^1.0.3:
has-symbols@^1.0.1, has-symbols@^1.0.2, has-symbols@^1.0.3:
  version "1.0.3"
  resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
  integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==


@@ 7946,6 8217,11 @@ md5.js@^1.3.4:
    inherits "^2.0.1"
    safe-buffer "^5.1.2"

mdn-data@2.0.14:
  version "2.0.14"
  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
  integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==

mdn-data@2.0.28:
  version "2.0.28"
  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.28.tgz#5ec48e7bef120654539069e1ae4ddc81ca490eba"


@@ 7956,6 8232,11 @@ mdn-data@2.0.30:
  resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc"
  integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==

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==

media-typer@0.3.0:
  version "0.3.0"
  resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"


@@ 8200,7 8481,7 @@ mixin-deep@^1.2.0:
    for-in "^1.0.2"
    is-extendable "^1.0.1"

mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6:
mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6, mkdirp@~0.5.1:
  version "0.5.6"
  resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
  integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==


@@ 8421,6 8702,13 @@ npmlog@^7.0.1:
    gauge "^5.0.0"
    set-blocking "^2.0.0"

nth-check@^1.0.2:
  version "1.0.2"
  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
  integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==
  dependencies:
    boolbase "~1.0.0"

nth-check@^2.0.1:
  version "2.1.1"
  resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"


@@ 8500,6 8788,17 @@ object.fromentries@^2.0.6:
    define-properties "^1.1.4"
    es-abstract "^1.20.4"

object.getownpropertydescriptors@^2.1.0:
  version "2.1.6"
  resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.6.tgz#5e5c384dd209fa4efffead39e3a0512770ccc312"
  integrity sha512-lq+61g26E/BgHv0ZTFgRvi7NMEPuAxLkFU7rukXjc/AlwH4Am5xXVnIXy3un1bg/JPbXHrixRkK1itUzzPiIjQ==
  dependencies:
    array.prototype.reduce "^1.0.5"
    call-bind "^1.0.2"
    define-properties "^1.2.0"
    es-abstract "^1.21.2"
    safe-array-concat "^1.0.0"

object.hasown@^1.1.2:
  version "1.1.2"
  resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.2.tgz#f919e21fad4eb38a57bc6345b3afd496515c3f92"


@@ 8515,7 8814,7 @@ object.pick@^1.3.0:
  dependencies:
    isobject "^3.0.1"

object.values@^1.1.6:
object.values@^1.1.0, object.values@^1.1.6:
  version "1.1.6"
  resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d"
  integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==


@@ 9455,6 9754,11 @@ pure-rand@^6.0.0:
  resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.2.tgz#a9c2ddcae9b68d736a8163036f088a2781c8b306"
  integrity sha512-6Yg0ekpKICSjPswYOuC5sku/TSWaRYlA0qsXqJgM/d/4pLPHPuTxK7Nbf7jFKzAeedUhR8C7K9Uv63FBsSo8xQ==

q@^1.1.2:
  version "1.5.1"
  resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
  integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==

qs@6.11.0:
  version "6.11.0"
  resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"


@@ 10210,6 10514,16 @@ rxjs@^7.8.0:
  dependencies:
    tslib "^2.1.0"

safe-array-concat@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060"
  integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ==
  dependencies:
    call-bind "^1.0.2"
    get-intrinsic "^1.2.0"
    has-symbols "^1.0.3"
    isarray "^2.0.5"

safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
  version "5.1.2"
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"


@@ 10261,6 10575,11 @@ sass@^1.62.1:
    immutable "^4.0.0"
    source-map-js ">=0.6.2 <2.0.0"

sax@~1.2.4:
  version "1.2.4"
  resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
  integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==

saxes@^6.0.0:
  version "6.0.0"
  resolved "https://registry.yarnpkg.com/saxes/-/saxes-6.0.0.tgz#fe5b4a4768df4f14a201b1ba6a65c1f3d9988cc5"


@@ 10747,6 11066,11 @@ ssri@^8.0.1:
  dependencies:
    minipass "^3.1.1"

stable@^0.1.8:
  version "0.1.8"
  resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
  integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==

stack-generator@^2.0.5:
  version "2.0.10"
  resolved "https://registry.yarnpkg.com/stack-generator/-/stack-generator-2.0.10.tgz#8ae171e985ed62287d4f1ed55a1633b3fb53bb4d"


@@ 11166,11 11490,35 @@ supports-preserve-symlinks-flag@^1.0.0:
  resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
  integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==

svg-parser@^2.0.2:
  version "2.0.4"
  resolved "https://registry.yarnpkg.com/svg-parser/-/svg-parser-2.0.4.tgz#fdc2e29e13951736140b76cb122c8ee6630eb6b5"
  integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==

svg-tags@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764"
  integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==

svgo@^1.2.2:
  version "1.3.2"
  resolved "https://registry.yarnpkg.com/svgo/-/svgo-1.3.2.tgz#b6dc511c063346c9e415b81e43401145b96d4167"
  integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==
  dependencies:
    chalk "^2.4.1"
    coa "^2.0.2"
    css-select "^2.0.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.1.0"
    sax "~1.2.4"
    stable "^0.1.8"
    unquote "~1.1.1"
    util.promisify "~1.0.0"

svgo@^3.0.2:
  version "3.0.2"
  resolved "https://registry.yarnpkg.com/svgo/-/svgo-3.0.2.tgz#5e99eeea42c68ee0dc46aa16da093838c262fe0a"


@@ 11546,6 11894,36 @@ type-is@~1.6.18:
    media-typer "0.3.0"
    mime-types "~2.1.24"

typed-array-buffer@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60"
  integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==
  dependencies:
    call-bind "^1.0.2"
    get-intrinsic "^1.2.1"
    is-typed-array "^1.1.10"

typed-array-byte-length@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0"
  integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==
  dependencies:
    call-bind "^1.0.2"
    for-each "^0.3.3"
    has-proto "^1.0.1"
    is-typed-array "^1.1.10"

typed-array-byte-offset@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b"
  integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==
  dependencies:
    available-typed-arrays "^1.0.5"
    call-bind "^1.0.2"
    for-each "^0.3.3"
    has-proto "^1.0.1"
    is-typed-array "^1.1.10"

typed-array-length@^1.0.4:
  version "1.0.4"
  resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb"


@@ 11656,6 12034,11 @@ unpipe@1.0.0, unpipe@~1.0.0:
  resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
  integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==

unquote@~1.1.1:
  version "1.1.1"
  resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"
  integrity sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==

unset-value@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"


@@ 11749,6 12132,16 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
  integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==

util.promisify@~1.0.0:
  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.3"
    es-abstract "^1.17.2"
    has-symbols "^1.0.1"
    object.getownpropertydescriptors "^2.1.0"

util@0.10.3:
  version "0.10.3"
  resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"


@@ 12124,6 12517,17 @@ which-module@^2.0.0:
  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
  integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==

which-typed-array@^1.1.10:
  version "1.1.11"
  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
  integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
  dependencies:
    available-typed-arrays "^1.0.5"
    call-bind "^1.0.2"
    for-each "^0.3.3"
    gopd "^1.0.1"
    has-tostringtag "^1.0.0"

which-typed-array@^1.1.9:
  version "1.1.9"
  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"