From f96e4b30473cf31acfa450a71b65dff02ea33d19 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 28 Jul 2023 23:02:08 +0200 Subject: [PATCH 01/29] Use original URL in preview if it redirects to 4xx page (#26200) --- app/services/fetch_link_card_service.rb | 14 +- spec/fixtures/requests/idn.txt | 483 ------------------ spec/services/fetch_link_card_service_spec.rb | 209 ++++++-- 3 files changed, 179 insertions(+), 527 deletions(-) delete mode 100644 spec/fixtures/requests/idn.txt diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb index dccd86c1e6..8865824609 100644 --- a/app/services/fetch_link_card_service.rb +++ b/app/services/fetch_link_card_service.rb @@ -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 diff --git a/spec/fixtures/requests/idn.txt b/spec/fixtures/requests/idn.txt deleted file mode 100644 index 5d07f2b796..0000000000 --- a/spec/fixtures/requests/idn.txt +++ /dev/null @@ -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 - - - - - - - - - - - - - - - 中国域名网站 - - - -
- -
- -
- - -
-
-
-

网址大全

- -
-
-
-
-
-

中文域名简介

-

- “中国域名”是中文域名的一种,特指以“中国”为后缀的中文域名,是我国域名体系和全球互联网域名体系的重要组成部分。“中国”是在全球互联网上代表中国的中文顶级域名,于2010年7月正式纳入全球互联网域名体系,全球互联网域名体系,全球网民可通过联网计算机在世界任何国家和地区实现无障碍访问。“中国”域名在使用上和 .CN,相似属于互联网上的基础服务,基于域名可以提供WWW.EMAIL FTP等应用服务。 -

-
- - - - diff --git a/spec/services/fetch_link_card_service_spec.rb b/spec/services/fetch_link_card_service_spec.rb index eba38b6998..f44cbb750c 100644 --- a/spec/services/fetch_link_card_service_spec.rb +++ b/spec/services/fetch_link_card_service_spec.rb @@ -5,96 +5,233 @@ require 'rails_helper' RSpec.describe FetchLinkCardService, type: :service do subject { described_class.new } + let(:html) { 'Hello world' } + 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) { '' } + + 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) { 'Hello world' } + 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 foo #Wannacry herumfliegen? - Ich will mal unter
https://github.com/qbi/WannaCry was sammeln. ! + Ich will mal unter
http://example.com/not-found was sammeln. ! security  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 From b0134db6ff43a1cbcf2775043c69916a2713c6eb Mon Sep 17 00:00:00 2001 From: gunchleoc Date: Fri, 28 Jul 2023 23:07:22 +0200 Subject: [PATCH 02/29] Allow filtering for Chinese languages (#26066) --- app/helpers/languages_helper.rb | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/helpers/languages_helper.rb b/app/helpers/languages_helper.rb index 840a18d3e6..1f400b477a 100644 --- a/app/helpers/languages_helper.rb +++ b/app/helpers/languages_helper.rb @@ -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) From 8d5d707cc1b7ca5461f628bde1e59e0c2096a771 Mon Sep 17 00:00:00 2001 From: Jay Prakash Kalia Date: Sat, 29 Jul 2023 02:39:19 +0530 Subject: [PATCH 03/29] Update README.md (#25435) --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 52ba7988d3..37cd3dfb46 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [releases]: https://github.com/mastodon/mastodon/releases [crowdin]: https://crowdin.com/project/mastodon -Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) +Mastodon is a **free, open-source social network server** based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub!) Click below to **learn more** in a video: @@ -51,7 +51,7 @@ Upload and view images and WebM/MP4 videos attached to the updates. Videos with ### Safety and moderation tools -Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) +Mastodon includes private posts, locked accounts, phrase filtering, muting, blocking, and all sorts of other features, along with a reporting and moderation system. [Learn more](https://blog.joinmastodon.org/2018/07/cage-the-mastodon/) ### OAuth2 and a straightforward REST API @@ -74,7 +74,7 @@ Mastodon acts as an OAuth2 provider, so 3rd party apps can use the REST and Stre The repository includes deployment configurations for **Docker and docker-compose** as well as specific platforms like **Heroku**, **Scalingo**, and **Nanobox**. For Helm charts, reference the [mastodon/chart repository](https://github.com/mastodon/chart). The [**standalone** installation guide](https://docs.joinmastodon.org/admin/install/) is available in the documentation. -A **Vagrant** configuration is included for development purposes. To use it, complete following steps: +A **Vagrant** configuration is included for development purposes. To use it, complete the following steps: - Install Vagrant and Virtualbox - Install the `vagrant-hostsupdater` plugin: `vagrant plugin install vagrant-hostsupdater` @@ -100,9 +100,9 @@ To get started, create a codespace for this repository by clicking this 👇 [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=52281283) -A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with software needed for this project. +A codespace will open in a web-based version of Visual Studio Code. The [dev container](.devcontainer/devcontainer.json) is fully configured with the software needed for this project. -**Note**: Dev containers is an open spec which is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting). +**Note**: Dev containers are an open spec that is supported by [GitHub Codespaces](https://github.com/codespaces) and [other tools](https://containers.dev/supporting). ## Contributing From 4d1b67f664e463f28ff45b8e125998ffcd2de50b Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Fri, 28 Jul 2023 23:09:49 +0200 Subject: [PATCH 04/29] Add end-to-end (system) tests (#25461) --- .github/workflows/test-ruby.yml | 97 +++++++++++++++++++++++++ Gemfile | 4 + Gemfile.lock | 11 +++ config/application.rb | 2 +- config/webpack/tests.js | 2 +- lib/tasks/spec.rake | 11 +++ spec/rails_helper.rb | 48 +++++++++++- spec/spec_helper.rb | 77 ++++++++++++++++++++ spec/support/stories/profile_stories.rb | 6 ++ spec/system/new_statuses_spec.rb | 45 ++++++++++++ 10 files changed, 298 insertions(+), 5 deletions(-) create mode 100644 lib/tasks/spec.rake create mode 100644 spec/system/new_statuses_spec.rb diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index ee9eefd458..ff135867f9 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -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/ diff --git a/Gemfile b/Gemfile index fcd10c5f9b..ff9a9cdb16 100644 --- a/Gemfile +++ b/Gemfile @@ -113,6 +113,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' diff --git a/Gemfile.lock b/Gemfile.lock index 5b1c62a692..fda288c6f0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) @@ -656,6 +660,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) @@ -768,6 +776,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 +813,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) @@ -885,6 +895,7 @@ DEPENDENCIES rubyzip (~> 2.3) sanitize (~> 6.0) scenic (~> 1.7) + selenium-webdriver sidekiq (~> 6.5) sidekiq-bulk (~> 0.2.0) sidekiq-scheduler (~> 5.0) diff --git a/config/application.rb b/config/application.rb index 6f21efa8db..436d7b3307 100644 --- a/config/application.rb +++ b/config/application.rb @@ -199,7 +199,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 diff --git a/config/webpack/tests.js b/config/webpack/tests.js index 1f7bdea9da..e6a8f1c2a9 100644 --- a/config/webpack/tests.js +++ b/config/webpack/tests.js @@ -5,5 +5,5 @@ const { merge } = require('webpack-merge'); const sharedConfig = require('./shared'); module.exports = merge(sharedConfig, { - mode: 'development', + mode: 'production', }); diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake new file mode 100644 index 0000000000..8f2cbeea35 --- /dev/null +++ b/lib/tasks/spec.rake @@ -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 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 2645f74e40..0f1073630d 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -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 diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 7b3af0f90b..dcbcad48e6 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -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 diff --git a/spec/support/stories/profile_stories.rb b/spec/support/stories/profile_stories.rb index de7ae17e63..2b345ddef1 100644 --- a/spec/support/stories/profile_stories.rb +++ b/spec/support/stories/profile_stories.rb @@ -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 diff --git a/spec/system/new_statuses_spec.rb b/spec/system/new_statuses_spec.rb new file mode 100644 index 0000000000..6faed6c808 --- /dev/null +++ b/spec/system/new_statuses_spec.rb @@ -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 From 30f5ec7303b0883be5b2f063ab0195c2d7eccf4c Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:11:05 -0400 Subject: [PATCH 05/29] Rubocop fix: `Perfomance/UnfreezeString` (#26217) --- .rubocop_todo.yml | 8 -------- app/lib/rss/builder.rb | 10 +++++++++- app/lib/text_formatter.rb | 2 +- app/validators/status_length_validator.rb | 2 +- app/views/well_known/host_meta/show.xml.ruby | 9 ++++++++- lib/tasks/mastodon.rake | 20 ++++++++++++++------ 6 files changed, 33 insertions(+), 18 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 472bb06229..9acedc0ab5 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -158,14 +158,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' diff --git a/app/lib/rss/builder.rb b/app/lib/rss/builder.rb index a9b3f08c5a..2e3697a2ab 100644 --- a/app/lib/rss/builder.rb +++ b/app/lib/rss/builder.rb @@ -14,13 +14,14 @@ class RSS::Builder end def to_xml - (''.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 diff --git a/app/lib/text_formatter.rb b/app/lib/text_formatter.rb index f9205e7d47..581ee835b3 100644 --- a/app/lib/text_formatter.rb +++ b/app/lib/text_formatter.rb @@ -75,7 +75,7 @@ class TextFormatter entity[:indices].first end - result = ''.dup + result = +'' last_index = entities.reduce(0) do |index, entity| indices = entity[:indices] diff --git a/app/validators/status_length_validator.rb b/app/validators/status_length_validator.rb index 727d24d919..dc841ded3e 100644 --- a/app/validators/status_length_validator.rb +++ b/app/validators/status_length_validator.rb @@ -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] diff --git a/app/views/well_known/host_meta/show.xml.ruby b/app/views/well_known/host_meta/show.xml.ruby index 25c5cf3948..125bfc9e06 100644 --- a/app/views/well_known/host_meta/show.xml.ruby +++ b/app/views/well_known/host_meta/show.xml.ruby @@ -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 -"#{Ox.dump(doc, effort: :tolerant)}".force_encoding('UTF-8') +Ox.dump(doc, effort: :tolerant).force_encoding('UTF-8') diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 19a369079e..010caaf8ea 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -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! From ad81be6c8ea4f0f3b7fcaff1d942bc13976c8926 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:11:45 -0400 Subject: [PATCH 06/29] Update rubocop rules for linelength (#26190) --- .rubocop.yml | 9 +---- .rubocop_todo.yml | 53 +++++----------------------- config/initializers/twitter_regex.rb | 4 +-- 3 files changed, 11 insertions(+), 55 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9ebb0219bf..c8a433c729 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 9acedc0ab5..0ffa4fb1cd 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -39,6 +39,13 @@ Layout/LeadingCommentSpace: - 'config/application.rb' - '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 @@ -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 # Configuration parameters: CountBlocks, Max. Metrics/BlockNesting: @@ -344,43 +350,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: @@ -918,9 +887,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 diff --git a/config/initializers/twitter_regex.rb b/config/initializers/twitter_regex.rb index 94db57b66d..9ca4eb0da9 100644 --- a/config/initializers/twitter_regex.rb +++ b/config/initializers/twitter_regex.rb @@ -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{(?: From 6602edf0643811241720bea12babf7576b11ce93 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:12:09 -0400 Subject: [PATCH 07/29] Add coverage for `LanguageValidator` (#25593) --- app/validators/language_validator.rb | 14 ++--- spec/validators/language_validator_spec.rb | 60 ++++++++++++++++++++++ 2 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 spec/validators/language_validator_spec.rb diff --git a/app/validators/language_validator.rb b/app/validators/language_validator.rb index b723e1a408..088bbc0976 100644 --- a/app/validators/language_validator.rb +++ b/app/validators/language_validator.rb @@ -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 diff --git a/spec/validators/language_validator_spec.rb b/spec/validators/language_validator_spec.rb new file mode 100644 index 0000000000..cb693dcd81 --- /dev/null +++ b/spec/validators/language_validator_spec.rb @@ -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 From 660993b415d928b45ae851b8e6d09d0e8af2f7f3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:12:25 -0400 Subject: [PATCH 08/29] Add coverage for `URLValidator` (#25591) --- app/validators/url_validator.rb | 23 +++++++-- spec/validators/url_validator_spec.rb | 70 +++++++++++++++++++-------- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index a90fb6958a..f88a370308 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -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 diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index f2220e32b0..4f32b7b399 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -2,32 +2,64 @@ require 'rails_helper' -RSpec.describe URLValidator, type: :validator do - describe '#validate_each' do - before do - allow(validator).to receive(:compliant?).with(value) { compliant } - validator.validate_each(record, attribute, value) +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 } - 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 } + describe '#validate_each' do + context 'with a nil value' do + it 'adds errors' do + record.profile = nil - context 'when not compliant?' do - let(:compliant) { false } - - 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 an invalid url scheme' do + it 'adds errors' do + record.profile = 'ftp://example.com/page' - it 'not calls errors.add' do - expect(errors).to_not have_received(:add).with(attribute, any_args) + 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 'with an unparseable value' do + it 'adds errors' do + record.profile = 'https://host:port/page' # non-numeric port string causes invalid uri error + + 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 'with a valid url' do + it 'does not add errors' do + record.profile = 'https://example.com/page' + + expect(record).to be_valid + expect(record.errors).to be_empty end end end From 6f1fa1364f40e991389718823fb3bcb1da30bbf5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:15:33 -0400 Subject: [PATCH 09/29] Fix `RSpec/EmptyExampleGroup` cop (#24735) --- .rubocop_todo.yml | 35 ------------------- spec/helpers/admin/action_logs_helper_spec.rb | 6 ---- spec/models/account_alias_spec.rb | 6 ---- spec/models/account_deletion_request_spec.rb | 6 ---- spec/models/account_moderation_note_spec.rb | 6 ---- spec/models/announcement_mute_spec.rb | 6 ---- spec/models/announcement_reaction_spec.rb | 6 ---- spec/models/announcement_spec.rb | 6 ---- spec/models/backup_spec.rb | 6 ---- spec/models/conversation_mute_spec.rb | 6 ---- spec/models/custom_filter_keyword_spec.rb | 6 ---- spec/models/custom_filter_spec.rb | 6 ---- spec/models/device_spec.rb | 6 ---- spec/models/encrypted_message_spec.rb | 6 ---- spec/models/featured_tag_spec.rb | 6 ---- .../follow_recommendation_suppression_spec.rb | 6 ---- spec/models/list_account_spec.rb | 6 ---- spec/models/list_spec.rb | 6 ---- spec/models/login_activity_spec.rb | 6 ---- spec/models/mute_spec.rb | 6 ---- spec/models/preview_card_spec.rb | 6 ---- spec/models/preview_card_trend_spec.rb | 6 ---- spec/models/relay_spec.rb | 6 ---- spec/models/scheduled_status_spec.rb | 6 ---- spec/models/status_stat_spec.rb | 6 ---- spec/models/status_trend_spec.rb | 6 ---- spec/models/system_key_spec.rb | 6 ---- spec/models/tag_follow_spec.rb | 6 ---- spec/models/unavailable_domain_spec.rb | 6 ---- spec/models/user_invite_request_spec.rb | 6 ---- spec/models/web/setting_spec.rb | 6 ---- spec/services/unmute_service_spec.rb | 7 ---- 32 files changed, 222 deletions(-) delete mode 100644 spec/helpers/admin/action_logs_helper_spec.rb delete mode 100644 spec/models/account_alias_spec.rb delete mode 100644 spec/models/account_deletion_request_spec.rb delete mode 100644 spec/models/account_moderation_note_spec.rb delete mode 100644 spec/models/announcement_mute_spec.rb delete mode 100644 spec/models/announcement_reaction_spec.rb delete mode 100644 spec/models/announcement_spec.rb delete mode 100644 spec/models/backup_spec.rb delete mode 100644 spec/models/conversation_mute_spec.rb delete mode 100644 spec/models/custom_filter_keyword_spec.rb delete mode 100644 spec/models/custom_filter_spec.rb delete mode 100644 spec/models/device_spec.rb delete mode 100644 spec/models/encrypted_message_spec.rb delete mode 100644 spec/models/featured_tag_spec.rb delete mode 100644 spec/models/follow_recommendation_suppression_spec.rb delete mode 100644 spec/models/list_account_spec.rb delete mode 100644 spec/models/list_spec.rb delete mode 100644 spec/models/login_activity_spec.rb delete mode 100644 spec/models/mute_spec.rb delete mode 100644 spec/models/preview_card_spec.rb delete mode 100644 spec/models/preview_card_trend_spec.rb delete mode 100644 spec/models/relay_spec.rb delete mode 100644 spec/models/scheduled_status_spec.rb delete mode 100644 spec/models/status_stat_spec.rb delete mode 100644 spec/models/status_trend_spec.rb delete mode 100644 spec/models/system_key_spec.rb delete mode 100644 spec/models/tag_follow_spec.rb delete mode 100644 spec/models/unavailable_domain_spec.rb delete mode 100644 spec/models/user_invite_request_spec.rb delete mode 100644 spec/models/web/setting_spec.rb delete mode 100644 spec/services/unmute_service_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 0ffa4fb1cd..972b496d5c 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -183,41 +183,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 diff --git a/spec/helpers/admin/action_logs_helper_spec.rb b/spec/helpers/admin/action_logs_helper_spec.rb deleted file mode 100644 index 4e9d08f09d..0000000000 --- a/spec/helpers/admin/action_logs_helper_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Admin::ActionLogsHelper do -end diff --git a/spec/models/account_alias_spec.rb b/spec/models/account_alias_spec.rb deleted file mode 100644 index f0cd274ea9..0000000000 --- a/spec/models/account_alias_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountAlias do -end diff --git a/spec/models/account_deletion_request_spec.rb b/spec/models/account_deletion_request_spec.rb deleted file mode 100644 index 8bbfb695d3..0000000000 --- a/spec/models/account_deletion_request_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountDeletionRequest do -end diff --git a/spec/models/account_moderation_note_spec.rb b/spec/models/account_moderation_note_spec.rb deleted file mode 100644 index 9d683ca528..0000000000 --- a/spec/models/account_moderation_note_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AccountModerationNote do -end diff --git a/spec/models/announcement_mute_spec.rb b/spec/models/announcement_mute_spec.rb deleted file mode 100644 index 1937da3aa7..0000000000 --- a/spec/models/announcement_mute_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AnnouncementMute do -end diff --git a/spec/models/announcement_reaction_spec.rb b/spec/models/announcement_reaction_spec.rb deleted file mode 100644 index 43cc0e1489..0000000000 --- a/spec/models/announcement_reaction_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe AnnouncementReaction do -end diff --git a/spec/models/announcement_spec.rb b/spec/models/announcement_spec.rb deleted file mode 100644 index 32d398213c..0000000000 --- a/spec/models/announcement_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Announcement do -end diff --git a/spec/models/backup_spec.rb b/spec/models/backup_spec.rb deleted file mode 100644 index 1303117528..0000000000 --- a/spec/models/backup_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Backup do -end diff --git a/spec/models/conversation_mute_spec.rb b/spec/models/conversation_mute_spec.rb deleted file mode 100644 index 3d5504a65c..0000000000 --- a/spec/models/conversation_mute_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ConversationMute do -end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb deleted file mode 100644 index 09a36fa3ed..0000000000 --- a/spec/models/custom_filter_keyword_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomFilterKeyword do -end diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb deleted file mode 100644 index f5e9b7ea96..0000000000 --- a/spec/models/custom_filter_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe CustomFilter do -end diff --git a/spec/models/device_spec.rb b/spec/models/device_spec.rb deleted file mode 100644 index 4e7b8f2e20..0000000000 --- a/spec/models/device_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Device do -end diff --git a/spec/models/encrypted_message_spec.rb b/spec/models/encrypted_message_spec.rb deleted file mode 100644 index c38142be1e..0000000000 --- a/spec/models/encrypted_message_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe EncryptedMessage do -end diff --git a/spec/models/featured_tag_spec.rb b/spec/models/featured_tag_spec.rb deleted file mode 100644 index 58865f94a5..0000000000 --- a/spec/models/featured_tag_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FeaturedTag do -end diff --git a/spec/models/follow_recommendation_suppression_spec.rb b/spec/models/follow_recommendation_suppression_spec.rb deleted file mode 100644 index d437d170d3..0000000000 --- a/spec/models/follow_recommendation_suppression_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe FollowRecommendationSuppression do -end diff --git a/spec/models/list_account_spec.rb b/spec/models/list_account_spec.rb deleted file mode 100644 index c9853d4a04..0000000000 --- a/spec/models/list_account_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ListAccount do -end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb deleted file mode 100644 index 621ad3968d..0000000000 --- a/spec/models/list_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe List do -end diff --git a/spec/models/login_activity_spec.rb b/spec/models/login_activity_spec.rb deleted file mode 100644 index 2214d3ef39..0000000000 --- a/spec/models/login_activity_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe LoginActivity do -end diff --git a/spec/models/mute_spec.rb b/spec/models/mute_spec.rb deleted file mode 100644 index 050083d0f8..0000000000 --- a/spec/models/mute_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Mute do -end diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb deleted file mode 100644 index 7d687d16ff..0000000000 --- a/spec/models/preview_card_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PreviewCard do -end diff --git a/spec/models/preview_card_trend_spec.rb b/spec/models/preview_card_trend_spec.rb deleted file mode 100644 index a31bf71cc0..0000000000 --- a/spec/models/preview_card_trend_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe PreviewCardTrend do -end diff --git a/spec/models/relay_spec.rb b/spec/models/relay_spec.rb deleted file mode 100644 index 7ed49e7334..0000000000 --- a/spec/models/relay_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Relay do -end diff --git a/spec/models/scheduled_status_spec.rb b/spec/models/scheduled_status_spec.rb deleted file mode 100644 index 286c17e696..0000000000 --- a/spec/models/scheduled_status_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe ScheduledStatus do -end diff --git a/spec/models/status_stat_spec.rb b/spec/models/status_stat_spec.rb deleted file mode 100644 index 9679c836a7..0000000000 --- a/spec/models/status_stat_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe StatusStat do -end diff --git a/spec/models/status_trend_spec.rb b/spec/models/status_trend_spec.rb deleted file mode 100644 index dbb3d4bb3f..0000000000 --- a/spec/models/status_trend_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe StatusTrend do -end diff --git a/spec/models/system_key_spec.rb b/spec/models/system_key_spec.rb deleted file mode 100644 index 5bd630aaa5..0000000000 --- a/spec/models/system_key_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe SystemKey do -end diff --git a/spec/models/tag_follow_spec.rb b/spec/models/tag_follow_spec.rb deleted file mode 100644 index 240147ecc8..0000000000 --- a/spec/models/tag_follow_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe TagFollow do -end diff --git a/spec/models/unavailable_domain_spec.rb b/spec/models/unavailable_domain_spec.rb deleted file mode 100644 index b868779f2b..0000000000 --- a/spec/models/unavailable_domain_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnavailableDomain do -end diff --git a/spec/models/user_invite_request_spec.rb b/spec/models/user_invite_request_spec.rb deleted file mode 100644 index ee0efbb431..0000000000 --- a/spec/models/user_invite_request_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UserInviteRequest do -end diff --git a/spec/models/web/setting_spec.rb b/spec/models/web/setting_spec.rb deleted file mode 100644 index 3182c67217..0000000000 --- a/spec/models/web/setting_spec.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Web::Setting do -end diff --git a/spec/services/unmute_service_spec.rb b/spec/services/unmute_service_spec.rb deleted file mode 100644 index 236f837e2d..0000000000 --- a/spec/services/unmute_service_spec.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe UnmuteService, type: :service do - subject { described_class.new } -end From 7581b1ff9664b90d1ba2cd75e7e332cc5bab4d36 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:16:23 -0400 Subject: [PATCH 10/29] Profiling tools configuration improvement (#25383) --- Gemfile | 4 ++++ Gemfile.lock | 4 ++++ spec/{ => fabricators}/fabricators_spec.rb | 0 spec/rails_helper.rb | 8 ++++++++ 4 files changed, 16 insertions(+) rename spec/{ => fabricators}/fabricators_spec.rb (100%) diff --git a/Gemfile b/Gemfile index ff9a9cdb16..a9080060b9 100644 --- a/Gemfile +++ b/Gemfile @@ -177,10 +177,14 @@ 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 :production do diff --git a/Gemfile.lock b/Gemfile.lock index fda288c6f0..151a372017 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -644,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) @@ -718,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) @@ -891,6 +893,7 @@ DEPENDENCIES rubocop-performance rubocop-rails rubocop-rspec + ruby-prof ruby-progressbar (~> 1.13) rubyzip (~> 2.3) sanitize (~> 6.0) @@ -908,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) diff --git a/spec/fabricators_spec.rb b/spec/fabricators/fabricators_spec.rb similarity index 100% rename from spec/fabricators_spec.rb rename to spec/fabricators/fabricators_spec.rb diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 0f1073630d..d4ff79c51c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -147,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| From 8ac9e446dfdf80cd1708232879f157d0301da3b9 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:17:35 -0400 Subject: [PATCH 11/29] Load `rspec-rails` gem in `test` + `development` (#25768) --- Gemfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index a9080060b9..478d2cfe99 100644 --- a/Gemfile +++ b/Gemfile @@ -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' @@ -187,6 +184,11 @@ group :development, :test do gem 'test-prof' end +group :development, :test do + # RSpec runner for rails + gem 'rspec-rails', '~> 6.0' +end + group :production do gem 'lograge', '~> 0.12' end From 74903af7ce6a0622752e19b1e0b326ecfa0463cb Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 28 Jul 2023 17:17:53 -0400 Subject: [PATCH 12/29] Ignore the brakeman `PermitAttributes` check (#25915) --- config/brakeman.ignore | 92 ------------------------------------------ config/brakeman.yml | 3 ++ 2 files changed, 3 insertions(+), 92 deletions(-) create mode 100644 config/brakeman.yml diff --git a/config/brakeman.ignore b/config/brakeman.ignore index 9b9330a758..02ce23a075 100644 --- a/config/brakeman.ignore +++ b/config/brakeman.ignore @@ -57,75 +57,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, @@ -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", diff --git a/config/brakeman.yml b/config/brakeman.yml new file mode 100644 index 0000000000..95ebc13865 --- /dev/null +++ b/config/brakeman.yml @@ -0,0 +1,3 @@ +--- +:skip_checks: + - CheckPermitAttributes From 4d3a12921254f992f649ad84f88ef947b9adc13d Mon Sep 17 00:00:00 2001 From: Plastikmensch Date: Sat, 29 Jul 2023 20:18:38 +0200 Subject: [PATCH 13/29] Fix public TL not indicating new toots when `onlyRemote` is enabled (#26247) Signed-off-by: Plastikmensch --- app/javascript/mastodon/features/public_timeline/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/public_timeline/index.jsx b/app/javascript/mastodon/features/public_timeline/index.jsx index 3bfb25ba73..c8f980e1d0 100644 --- a/app/javascript/mastodon/features/public_timeline/index.jsx +++ b/app/javascript/mastodon/features/public_timeline/index.jsx @@ -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, From d76f79f64706f6e9f48d9a9cef2f049f1aed09e5 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Sat, 29 Jul 2023 22:28:05 +0200 Subject: [PATCH 14/29] Remove obsolete backport from Rails 7 (#26254) --- config/application.rb | 1 - .../conditional_get_extensions.rb | 15 --------------- 2 files changed, 16 deletions(-) delete mode 100644 lib/action_controller/conditional_get_extensions.rb diff --git a/config/application.rb b/config/application.rb index 436d7b3307..a3bd3dfa4e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -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' diff --git a/lib/action_controller/conditional_get_extensions.rb b/lib/action_controller/conditional_get_extensions.rb deleted file mode 100644 index 192ed341ea..0000000000 --- a/lib/action_controller/conditional_get_extensions.rb +++ /dev/null @@ -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) From d4807a5e645b9363c7f8d6702186df4325bc5a97 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 30 Jul 2023 03:35:17 +0200 Subject: [PATCH 15/29] Change aspect ratios on link previews in web UI (#26250) --- .../features/status/components/card.jsx | 82 +++++++------------ .../styles/mastodon/components.scss | 76 +++++++++++++++-- 2 files changed, 97 insertions(+), 61 deletions(-) diff --git a/app/javascript/mastodon/features/status/components/card.jsx b/app/javascript/mastodon/features/status/components/card.jsx index 29a12c87bf..6ac3c1d0f2 100644 --- a/app/javascript/mastodon/features/status/components/card.jsx +++ b/app/javascript/mastodon/features/status/components/card.jsx @@ -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 (
); } @@ -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 = (
{provider} {card.get('published_at') && <> · } - + + {card.get('title')} - {card.get('author_name').length > 0 && {card.get('author_name')} }} />} + + {card.get('author_name').length > 0 ? {card.get('author_name')} }} /> : {card.get('description')}}
); 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 = ( +
{spoilerButton}
); @@ -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 = (
{canvas} {thumbnail} - {revealed && ( -
+ {revealed ? ( +
- +
- )} - - {!revealed && spoilerButton} + ) : spoilerButton}
); } return ( -
+
{embed} {description}
@@ -244,14 +220,14 @@ export default class Card extends PureComponent { ); } else { embed = ( -
+
); } return ( - + {embed} {description} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index d16380de5b..bc325edf6c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3510,13 +3510,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 +3530,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 +3577,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 +3593,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 +3627,30 @@ 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; + -webkit-line-clamp: 2; } .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 +3658,33 @@ 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; + border-start-end-radius: 0; + border-end-end-radius: 0; & > .fa { font-size: 21px; @@ -3664,6 +3698,8 @@ a.status-card { .status-card__image-image { border-radius: 8px; + border-start-end-radius: 0; + border-end-end-radius: 0; display: block; margin: 0; width: 100%; @@ -3675,6 +3711,8 @@ a.status-card { .status-card__image-preview { border-radius: 8px; + border-start-end-radius: 0; + border-end-end-radius: 0; display: block; margin: 0; width: 100%; @@ -3691,6 +3729,28 @@ 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.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; @@ -4902,7 +4962,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 +8278,7 @@ noscript { flex: 0 0 auto; position: relative; width: 120px; - height: 120px; + aspect-ratio: 1; .skeleton { width: 100%; From d82abc877a65234f453112f7530315ce03cee740 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 31 Jul 2023 11:17:41 +0200 Subject: [PATCH 16/29] Fix `Importer::BaseImporter#clean_up!` not using proper primary key (#26269) --- app/lib/importer/base_importer.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/lib/importer/base_importer.rb b/app/lib/importer/base_importer.rb index 0cd1d34227..07be4650ec 100644 --- a/app/lib/importer/base_importer.rb +++ b/app/lib/importer/base_importer.rb @@ -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? From 51768de16eb4e27d7813cf28065078a392fcf327 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 31 Jul 2023 21:11:25 +0200 Subject: [PATCH 17/29] Bump version to v4.1.6 (#26272) --- CHANGELOG.md | 8 ++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c49b192735..9fe1ac3803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index e3fa9ae1e2..ae6aefb37f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 17af0c051a..03e8eb1435 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -13,7 +13,7 @@ module Mastodon end def patch - 5 + 6 end def flags From fd284311e79854d6bc2901a9d9363ba9e7e00513 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Mon, 31 Jul 2023 23:17:37 +0200 Subject: [PATCH 18/29] Do not normalize URL before fetching it (#26219) --- app/lib/request.rb | 17 +++- .../concerns/signature_verification_spec.rb | 33 ++++++- spec/lib/request_spec.rb | 97 ++++++++++++++++++- 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index c7aafbd4fc..e3597b052f 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -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(uri.path), + query: 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) @@ -139,7 +152,7 @@ class Request 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 diff --git a/spec/controllers/concerns/signature_verification_spec.rb b/spec/controllers/concerns/signature_verification_spec.rb index 650cd21eaf..f8e44845d7 100644 --- a/spec/controllers/concerns/signature_verification_spec.rb +++ b/spec/controllers/concerns/signature_verification_spec.rb @@ -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 diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index f0861376b9..1e16f60fbd 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -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,99 @@ describe Request do expect { subject.perform }.to raise_error Mastodon::ValidationError 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 modify path' do + subject.perform do |response| + expect(response.request.uri.path).to eq '/foo%41%3A' + end + end + + it 'does modify query string' do + subject.perform do |response| + expect(response.request.uri.query).to eq 'bar=%41%3A' + 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/föo?bär=1' } + + before do + stub_request(:get, 'http://xn--xample-9ua.com/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' + end + end + + it 'percent-escapes path and query string' do + subject.perform + + expect(a_request(:get, 'http://xn--xample-9ua.com/f%C3%B6o?b%C3%A4r=1')).to have_been_made + 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 From 6c39125761faf756f56a4183b2794c85c9a37b74 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 1 Aug 2023 14:52:32 +0200 Subject: [PATCH 19/29] Change /api/v1/peers/search to be case-insensitive when using Elasticsearch (#26268) --- app/controllers/api/v1/peers/search_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index 50a342cde3..bd72b985f6 100644 --- a/app/controllers/api/v1/peers/search_controller.rb +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -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), }, }, From 71fd70335adecfc53c3dd2d57e1483ff60adb530 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 1 Aug 2023 17:11:30 +0200 Subject: [PATCH 20/29] Change interaction modal input to disable browser spell-checking, capitalization and autocomplete (#26267) --- app/javascript/mastodon/features/interaction_modal/index.jsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/javascript/mastodon/features/interaction_modal/index.jsx b/app/javascript/mastodon/features/interaction_modal/index.jsx index 6e17ab0194..6b6768ac32 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.jsx +++ b/app/javascript/mastodon/features/interaction_modal/index.jsx @@ -250,6 +250,9 @@ class LoginForm extends React.PureComponent { onFocus={this.handleFocus} onBlur={this.handleBlur} onKeyDown={this.handleKeyDown} + autocomplete='off' + autocapitalize='off' + spellcheck='false' /> From f2257069b21892f8bbd57595efb474cdd1020bbc Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Tue, 1 Aug 2023 19:34:11 +0200 Subject: [PATCH 21/29] Fix AVIF attachments (#26264) --- app/models/media_attachment.rb | 2 +- config/imagemagick/policy.xml | 2 +- .../media_type_spoof_detector_extensions.rb | 4 +- spec/fixtures/files/600x400.avif | Bin 0 -> 7742 bytes spec/fixtures/files/600x400.heic | Bin 0 -> 9671 bytes spec/fixtures/files/600x400.jpeg | Bin 0 -> 21442 bytes spec/fixtures/files/600x400.png | Bin 0 -> 14127 bytes spec/fixtures/files/600x400.webp | Bin 0 -> 9026 bytes spec/models/media_attachment_spec.rb | 115 +++++++++++++----- 9 files changed, 87 insertions(+), 36 deletions(-) create mode 100644 spec/fixtures/files/600x400.avif create mode 100644 spec/fixtures/files/600x400.heic create mode 100644 spec/fixtures/files/600x400.jpeg create mode 100644 spec/fixtures/files/600x400.png create mode 100644 spec/fixtures/files/600x400.webp diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 8689a956e9..7474b5653f 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -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 diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml index 1052476b31..e2aa202f27 100644 --- a/config/imagemagick/policy.xml +++ b/config/imagemagick/policy.xml @@ -22,6 +22,6 @@ - + diff --git a/lib/paperclip/media_type_spoof_detector_extensions.rb b/lib/paperclip/media_type_spoof_detector_extensions.rb index a406ef312f..c8868d5e95 100644 --- a/lib/paperclip/media_type_spoof_detector_extensions.rb +++ b/lib/paperclip/media_type_spoof_detector_extensions.rb @@ -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 diff --git a/spec/fixtures/files/600x400.avif b/spec/fixtures/files/600x400.avif new file mode 100644 index 0000000000000000000000000000000000000000..f306942dbee737c381c69dbfb69782de3a688b3b GIT binary patch literal 7742 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/600x400.jpeg b/spec/fixtures/files/600x400.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..1c20bea515d236fa12c71f7a606f0de062bfc93f GIT binary patch literal 21442 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 F|Jf;WyQlyF literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/600x400.png b/spec/fixtures/files/600x400.png new file mode 100644 index 0000000000000000000000000000000000000000..3b8a2dee352101066c9d59e65f141847043f6932 GIT binary patch literal 14127 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 literal 0 HcmV?d00001 diff --git a/spec/fixtures/files/600x400.webp b/spec/fixtures/files/600x400.webp new file mode 100644 index 0000000000000000000000000000000000000000..56fa5151c2dfbae7ab980f70f74e42090e0341fe GIT binary patch literal 9026 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 aa7UKncL9_c;@mPXk0X!-RR2E80R9i0cfmsd literal 0 HcmV?d00001 diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index 90e4f2f47b..d142875aa9 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -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 From ca342d48389de72e2c299c613a5a0e1deebf0093 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Tue, 1 Aug 2023 19:34:40 +0200 Subject: [PATCH 22/29] Add List-Unsubscribe email header (#26085) --- .../mail_subscriptions_controller.rb | 5 +- app/mailers/notification_mailer.rb | 8 ++ app/views/layouts/mailer.html.haml | 4 +- spec/mailers/notification_mailer_spec.rb | 43 ++++++-- spec/requests/mail_subscriptions_spec.rb | 103 ++++++++++++++++++ 5 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 spec/requests/mail_subscriptions_spec.rb diff --git a/app/controllers/mail_subscriptions_controller.rb b/app/controllers/mail_subscriptions_controller.rb index b071a80605..1caeaaacf4 100644 --- a/app/controllers/mail_subscriptions_controller.rb +++ b/app/controllers/mail_subscriptions_controller.rb @@ -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 diff --git a/app/mailers/notification_mailer.rb b/app/mailers/notification_mailer.rb index 277612366b..5eecfed104 100644 --- a/app/mailers/notification_mailer.rb +++ b/app/mailers/notification_mailer.rb @@ -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? diff --git a/app/views/layouts/mailer.html.haml b/app/views/layouts/mailer.html.haml index e39a09780e..7fa344a9b7 100644 --- a/app/views/layouts/mailer.html.haml +++ b/app/views/layouts/mailer.html.haml @@ -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 diff --git a/spec/mailers/notification_mailer_spec.rb b/spec/mailers/notification_mailer_spec.rb index 636c2d4257..78a497c06b 100644 --- a/spec/mailers/notification_mailer_spec.rb +++ b/spec/mailers/notification_mailer_spec.rb @@ -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{}) + 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(//) + expect(mail['References'].value).to match(//) + 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 diff --git a/spec/requests/mail_subscriptions_spec.rb b/spec/requests/mail_subscriptions_spec.rb new file mode 100644 index 0000000000..cc6557cab0 --- /dev/null +++ b/spec/requests/mail_subscriptions_spec.rb @@ -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 From 4c999a736c09223418749b85304b128e746be64d Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 2 Aug 2023 01:57:31 +0200 Subject: [PATCH 23/29] Fix wrong border radius on link cards in web UI (#26287) --- app/javascript/styles/mastodon/components.scss | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index bc325edf6c..713805024b 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3682,9 +3682,6 @@ a.status-card { aspect-ratio: 1; background: lighten($ui-base-color, 8%); position: relative; - border-radius: 8px; - border-start-end-radius: 0; - border-end-end-radius: 0; & > .fa { font-size: 21px; @@ -3697,9 +3694,6 @@ a.status-card { } .status-card__image-image { - border-radius: 8px; - border-start-end-radius: 0; - border-end-end-radius: 0; display: block; margin: 0; width: 100%; @@ -3710,9 +3704,6 @@ a.status-card { } .status-card__image-preview { - border-radius: 8px; - border-start-end-radius: 0; - border-end-end-radius: 0; display: block; margin: 0; width: 100%; @@ -3739,6 +3730,15 @@ a.status-card { 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 { From 01f0cffc2c5b14e0a7932539896783cc11b2a052 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 2 Aug 2023 04:17:23 +0200 Subject: [PATCH 24/29] Fix line clamp for link previews in web UI (#26286) --- app/javascript/styles/mastodon/components.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 713805024b..7fe7a00412 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3633,7 +3633,9 @@ a.status-card { .status-card.expanded .status-card__title { white-space: normal; + display: -webkit-box; -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } .status-card__content { From 2cbdff97cedcf87f567effa782e0314c849334c6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Wed, 2 Aug 2023 17:24:32 +0200 Subject: [PATCH 25/29] Change design of role badges in web UI (#26281) Co-authored-by: Claire --- app/javascript/mastodon/components/badge.jsx | 34 ++ .../features/account/components/header.jsx | 22 +- app/javascript/styles/mastodon/accounts.scss | 25 +- config/webpack/rules/index.js | 2 + config/webpack/rules/material_icons.js | 13 + package.json | 2 + yarn.lock | 424 +++++++++++++++++- 7 files changed, 491 insertions(+), 31 deletions(-) create mode 100644 app/javascript/mastodon/components/badge.jsx create mode 100644 config/webpack/rules/material_icons.js diff --git a/app/javascript/mastodon/components/badge.jsx b/app/javascript/mastodon/components/badge.jsx new file mode 100644 index 0000000000..235aef0c26 --- /dev/null +++ b/app/javascript/mastodon/components/badge.jsx @@ -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 }) => ( +
+ {icon} + {label} + {domain && {domain}} +
+); + +Badge.propTypes = { + icon: PropTypes.node, + label: PropTypes.node, + domain: PropTypes.node, +}; + +Badge.defaultProps = { + icon: , +}; + +export const GroupBadge = () => ( + } label={} /> +); + +export const AutomatedBadge = () => ( + } label={} /> +); \ No newline at end of file diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index 51206c03bf..d351e210f6 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -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( -
- { ' ' } - -
- ); + badges.push(); } else if (account.get('group')) { - badges.push( -
- { ' ' } - -
- ); + badges.push(); } account.get('roles', []).forEach((role) => { - badges.push( -
- { ' ' } - {role.get('name')} ({domain}) -
- ); + badges.push({role.get('name')}} domain={domain} />); }); return ( diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index babfbbbad0..2a5285ee02 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -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; - .fa { - color: var(--user-role-accent, $highlight-text-color); + svg { + width: auto; + height: 15px; + opacity: 0.85; + fill: currentColor; + } + + &__domain { + font-weight: 400; + opacity: 0.75; + letter-spacing: 0; } } diff --git a/config/webpack/rules/index.js b/config/webpack/rules/index.js index 7e1857341c..b026857887 100644 --- a/config/webpack/rules/index.js +++ b/config/webpack/rules/index.js @@ -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, diff --git a/config/webpack/rules/material_icons.js b/config/webpack/rules/material_icons.js new file mode 100644 index 0000000000..f53445ef79 --- /dev/null +++ b/config/webpack/rules/material_icons.js @@ -0,0 +1,13 @@ +module.exports = { + test: /\.svg$/, + include: /node_modules\/@material-design-icons/, + issuer: /\.[jt]sx?$/, + use: [ + { + loader: '@svgr/webpack', + options: { + svgo: false, + }, + }, + ], +}; diff --git a/package.json b/package.json index b46dada7d0..72a378299f 100644 --- a/package.json +++ b/package.json @@ -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", "arrow-key-navigation": "^1.2.0", "async-mutex": "^0.4.0", diff --git a/yarn.lock b/yarn.lock index 1d9df1e5e1..3cf8dc7072 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -3441,7 +3584,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== @@ -3757,7 +3900,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== @@ -3929,6 +4072,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" @@ -4280,6 +4432,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" @@ -4291,6 +4458,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" @@ -4307,6 +4490,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" @@ -4370,6 +4558,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" @@ -4743,6 +4938,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" @@ -4757,7 +4960,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== @@ -4776,6 +4984,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" @@ -4903,6 +5119,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" @@ -4929,6 +5150,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" @@ -4969,6 +5235,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" @@ -5835,7 +6106,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== @@ -6092,7 +6363,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== @@ -7931,6 +8202,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" @@ -7941,6 +8217,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" @@ -8185,7 +8466,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== @@ -8406,6 +8687,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" @@ -8485,6 +8773,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" @@ -8500,7 +8799,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== @@ -9440,6 +9739,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" @@ -10195,6 +10499,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" @@ -10246,6 +10560,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" @@ -10732,6 +11051,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" @@ -11151,11 +11475,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" @@ -11531,6 +11879,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" @@ -11641,6 +12019,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" @@ -11734,6 +12117,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" @@ -12109,6 +12502,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" From 8891d8945d837f0da16a3a5aa2dc9783e39b0acd Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Wed, 2 Aug 2023 19:32:29 +0200 Subject: [PATCH 26/29] Fix request URL normalisation for bare domain and 8-bit characters (#26285) --- app/lib/request.rb | 10 ++++-- spec/lib/request_spec.rb | 67 +++++++++++++++++++++++++++++++++++----- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index e3597b052f..adc9a48f3d 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -76,8 +76,8 @@ class Request HTTP::URI.new( scheme: uri.normalized_scheme, authority: uri.normalized_authority, - path: Addressable::URI.normalize_path(uri.path), - query: uri.query + path: Addressable::URI.normalize_path(encode_non_ascii(uri.path)).presence || '/', + query: encode_non_ascii(uri.query) ) end @@ -151,6 +151,12 @@ 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).use(normalize_uri: { normalizer: URI_NORMALIZER }).follow(max_hops: 3) end diff --git a/spec/lib/request_spec.rb b/spec/lib/request_spec.rb index 1e16f60fbd..8ccfcacef2 100644 --- a/spec/lib/request_spec.rb +++ b/spec/lib/request_spec.rb @@ -95,6 +95,33 @@ describe Request do 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' } @@ -114,18 +141,31 @@ describe Request do end end - it 'does modify path' do + it 'does not modify path' do subject.perform do |response| expect(response.request.uri.path).to eq '/foo%41%3A' end end - it 'does modify query string' do + 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 @@ -134,22 +174,35 @@ describe Request do end context 'with non-ASCII URL' do - let(:url) { 'http://éxample.com/föo?bär=1' } + let(:url) { 'http://éxample.com:81/föo?bär=1' } before do - stub_request(:get, 'http://xn--xample-9ua.com/f%C3%B6o?b%C3%A4r=1') + 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' + expect(response.request.uri.authority).to eq 'xn--xample-9ua.com:81' end end - it 'percent-escapes path and query string' do + 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 - expect(a_request(:get, 'http://xn--xample-9ua.com/f%C3%B6o?b%C3%A4r=1')).to have_been_made + 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 From e258b4cb64479fffbede4763dffe0379d0798f8e Mon Sep 17 00:00:00 2001 From: Emelia Smith Date: Wed, 2 Aug 2023 19:32:48 +0200 Subject: [PATCH 27/29] Refactor: replace whitelist_mode mentions with limited_federation_mode (#26252) --- .rubocop_todo.yml | 2 +- app/controllers/accounts_controller.rb | 2 +- app/controllers/admin/instances_controller.rb | 2 +- app/controllers/api/base_controller.rb | 4 ++-- app/controllers/api/v1/instances/activity_controller.rb | 4 ++-- .../api/v1/instances/domain_blocks_controller.rb | 2 +- .../api/v1/instances/extended_descriptions_controller.rb | 4 ++-- app/controllers/api/v1/instances/peers_controller.rb | 6 +++--- .../api/v1/instances/privacy_policies_controller.rb | 2 +- app/controllers/api/v1/instances/rules_controller.rb | 4 ++-- .../api/v1/instances/translation_languages_controller.rb | 2 +- app/controllers/api/v1/instances_controller.rb | 4 ++-- app/controllers/api/v1/peers/search_controller.rb | 4 ++-- app/controllers/application_controller.rb | 4 ++-- app/controllers/concerns/account_owned_concern.rb | 2 +- app/controllers/concerns/api_caching_concern.rb | 2 +- app/controllers/follower_accounts_controller.rb | 2 +- app/controllers/following_accounts_controller.rb | 2 +- app/controllers/media_controller.rb | 4 ++-- app/controllers/media_proxy_controller.rb | 2 +- app/controllers/statuses_controller.rb | 2 +- app/controllers/tags_controller.rb | 4 ++-- app/helpers/domain_control_helper.rb | 6 +++--- app/serializers/initial_state_serializer.rb | 2 +- app/services/concerns/payloadable.rb | 2 +- app/services/unallow_domain_service.rb | 2 +- app/views/admin/instances/index.html.haml | 6 +++--- app/views/admin/instances/show.html.haml | 2 +- config/initializers/2_limited_federation_mode.rb | 7 +++++++ config/initializers/2_whitelist_mode.rb | 5 ----- config/navigation.rb | 4 ++-- spec/requests/cache_spec.rb | 6 +++--- spec/services/unallow_domain_service_spec.rb | 4 ++-- 33 files changed, 57 insertions(+), 55 deletions(-) create mode 100644 config/initializers/2_limited_federation_mode.rb delete mode 100644 config/initializers/2_whitelist_mode.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 972b496d5c..b942165129 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -589,7 +589,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' diff --git a/app/controllers/accounts_controller.rb b/app/controllers/accounts_controller.rb index 0090ef7ec5..936973fb2a 100644 --- a/app/controllers/accounts_controller.rb +++ b/app/controllers/accounts_controller.rb @@ -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| diff --git a/app/controllers/admin/instances_controller.rb b/app/controllers/admin/instances_controller.rb index 5194057263..e5a55de06d 100644 --- a/app/controllers/admin/instances_controller.rb +++ b/app/controllers/admin/instances_controller.rb @@ -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 diff --git a/app/controllers/api/base_controller.rb b/app/controllers/api/base_controller.rb index 2629ab782f..c764b45101 100644 --- a/app/controllers/api/base_controller.rb +++ b/app/controllers/api/base_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/activity_controller.rb b/app/controllers/api/v1/instances/activity_controller.rb index 3d55d990af..9da77f8dab 100644 --- a/app/controllers/api/v1/instances/activity_controller.rb +++ b/app/controllers/api/v1/instances/activity_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/domain_blocks_controller.rb b/app/controllers/api/v1/instances/domain_blocks_controller.rb index e954c45897..c91234e088 100644 --- a/app/controllers/api/v1/instances/domain_blocks_controller.rb +++ b/app/controllers/api/v1/instances/domain_blocks_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/extended_descriptions_controller.rb b/app/controllers/api/v1/instances/extended_descriptions_controller.rb index a0665725bd..376fec9066 100644 --- a/app/controllers/api/v1/instances/extended_descriptions_controller.rb +++ b/app/controllers/api/v1/instances/extended_descriptions_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/peers_controller.rb b/app/controllers/api/v1/instances/peers_controller.rb index 23096650e6..08a982f227 100644 --- a/app/controllers/api/v1/instances/peers_controller.rb +++ b/app/controllers/api/v1/instances/peers_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/privacy_policies_controller.rb b/app/controllers/api/v1/instances/privacy_policies_controller.rb index 36889f7335..f5b1b4ec5f 100644 --- a/app/controllers/api/v1/instances/privacy_policies_controller.rb +++ b/app/controllers/api/v1/instances/privacy_policies_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/rules_controller.rb b/app/controllers/api/v1/instances/rules_controller.rb index d3eeca3262..2f71984b05 100644 --- a/app/controllers/api/v1/instances/rules_controller.rb +++ b/app/controllers/api/v1/instances/rules_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances/translation_languages_controller.rb b/app/controllers/api/v1/instances/translation_languages_controller.rb index c4680cccb8..78423e40e4 100644 --- a/app/controllers/api/v1/instances/translation_languages_controller.rb +++ b/app/controllers/api/v1/instances/translation_languages_controller.rb @@ -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 diff --git a/app/controllers/api/v1/instances_controller.rb b/app/controllers/api/v1/instances_controller.rb index 5a6701ff96..df4a14af15 100644 --- a/app/controllers/api/v1/instances_controller.rb +++ b/app/controllers/api/v1/instances_controller.rb @@ -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 diff --git a/app/controllers/api/v1/peers/search_controller.rb b/app/controllers/api/v1/peers/search_controller.rb index bd72b985f6..2c0eacdcae 100644 --- a/app/controllers/api/v1/peers/search_controller.rb +++ b/app/controllers/api/v1/peers/search_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 66886b4519..975315e247 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,7 +19,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? @@ -52,7 +52,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? diff --git a/app/controllers/concerns/account_owned_concern.rb b/app/controllers/concerns/account_owned_concern.rb index 25149d03fb..3fc0938bfc 100644 --- a/app/controllers/concerns/account_owned_concern.rb +++ b/app/controllers/concerns/account_owned_concern.rb @@ -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? diff --git a/app/controllers/concerns/api_caching_concern.rb b/app/controllers/concerns/api_caching_concern.rb index 705abce80f..12264d514e 100644 --- a/app/controllers/concerns/api_caching_concern.rb +++ b/app/controllers/concerns/api_caching_concern.rb @@ -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 diff --git a/app/controllers/follower_accounts_controller.rb b/app/controllers/follower_accounts_controller.rb index f35af5903c..ffdbd01802 100644 --- a/app/controllers/follower_accounts_controller.rb +++ b/app/controllers/follower_accounts_controller.rb @@ -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| diff --git a/app/controllers/following_accounts_controller.rb b/app/controllers/following_accounts_controller.rb index 2aa31bdf08..cce296f9fd 100644 --- a/app/controllers/following_accounts_controller.rb +++ b/app/controllers/following_accounts_controller.rb @@ -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| diff --git a/app/controllers/media_controller.rb b/app/controllers/media_controller.rb index 3bf5b7eba7..53eee40012 100644 --- a/app/controllers/media_controller.rb +++ b/app/controllers/media_controller.rb @@ -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 diff --git a/app/controllers/media_proxy_controller.rb b/app/controllers/media_proxy_controller.rb index 8d480d704e..c4230d62c3 100644 --- a/app/controllers/media_proxy_controller.rb +++ b/app/controllers/media_proxy_controller.rb @@ -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 diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index 1ff0fbd600..effaba3630 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -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) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 7e249dbea5..2007fe8462 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -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| diff --git a/app/helpers/domain_control_helper.rb b/app/helpers/domain_control_helper.rb index 6fce7eb1f5..2703c1b8f8 100644 --- a/app/helpers/domain_control_helper.rb +++ b/app/helpers/domain_control_helper.rb @@ -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 diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index b90db4f58f..bda06ef1f1 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -22,7 +22,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, diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 04c3798fe0..1389a42ed6 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -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 diff --git a/app/services/unallow_domain_service.rb b/app/services/unallow_domain_service.rb index fc52607612..bdc71b1c08 100644 --- a/app/services/unallow_domain_service.rb +++ b/app/services/unallow_domain_service.rb @@ -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 diff --git a/app/views/admin/instances/index.html.haml b/app/views/admin/instances/index.html.haml index 0bae70e31d..189dddcd29 100644 --- a/app/views/admin/instances/index.html.haml +++ b/app/views/admin/instances/index.html.haml @@ -5,7 +5,7 @@ = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous' - 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' @@ -20,7 +20,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 @@ -30,7 +30,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| diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml index 6d67d389d2..3e4c41f737 100644 --- a/app/views/admin/instances/show.html.haml +++ b/app/views/admin/instances/show.html.haml @@ -36,7 +36,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 diff --git a/config/initializers/2_limited_federation_mode.rb b/config/initializers/2_limited_federation_mode.rb new file mode 100644 index 0000000000..d5f7652d58 --- /dev/null +++ b/config/initializers/2_limited_federation_mode.rb @@ -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 diff --git a/config/initializers/2_whitelist_mode.rb b/config/initializers/2_whitelist_mode.rb deleted file mode 100644 index 1cc6a8e724..0000000000 --- a/config/initializers/2_whitelist_mode.rb +++ /dev/null @@ -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 diff --git a/config/navigation.rb b/config/navigation.rb index c4914cd995..4c1396cc86 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -40,7 +40,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) } @@ -54,7 +54,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) } diff --git a/spec/requests/cache_spec.rb b/spec/requests/cache_spec.rb index 902f21db4b..178d19ed0d 100644 --- a/spec/requests/cache_spec.rb +++ b/spec/requests/cache_spec.rb @@ -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 diff --git a/spec/services/unallow_domain_service_spec.rb b/spec/services/unallow_domain_service_spec.rb index f27b6fdf39..19d40e7e86 100644 --- a/spec/services/unallow_domain_service_spec.rb +++ b/spec/services/unallow_domain_service_spec.rb @@ -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 From 6308dca76aef6dd8265813d58f7aab3f96eb21cc Mon Sep 17 00:00:00 2001 From: Trevor Wolf Date: Thu, 3 Aug 2023 03:33:41 +1000 Subject: [PATCH 28/29] change column link to add a better keyboard focus indicator (#26278) --- app/javascript/styles/mastodon/basics.scss | 2 +- app/javascript/styles/mastodon/components.scss | 9 ++++++++- app/javascript/styles/mastodon/variables.scss | 4 +++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/javascript/styles/mastodon/basics.scss b/app/javascript/styles/mastodon/basics.scss index a77f8425dd..6714b24268 100644 --- a/app/javascript/styles/mastodon/basics.scss +++ b/app/javascript/styles/mastodon/basics.scss @@ -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) { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 7fe7a00412..76928ba1fa 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -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; @@ -3958,7 +3965,7 @@ a.status-card { } &:focus-visible { - outline: $ui-button-icon-focus-outline; + outline: $ui-button-focus-outline; } &.active { diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss index 073bb16e59..611c8bb5d1 100644 --- a/app/javascript/styles/mastodon/variables.scss +++ b/app/javascript/styles/mastodon/variables.scss @@ -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 From 425d77f8124a50fc033e8fb3bdf7b89a6a25f4fa Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 2 Aug 2023 20:54:56 +0200 Subject: [PATCH 29/29] Fix crash in `tootctl status remove` and some old migrations (#26210) --- db/migrate/20180812173710_copy_status_stats.rb | 2 +- db/migrate/20181116173541_copy_account_stats.rb | 2 +- lib/mastodon/cli/statuses.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/migrate/20180812173710_copy_status_stats.rb b/db/migrate/20180812173710_copy_status_stats.rb index 20baeeca6b..52ab43b762 100644 --- a/db/migrate/20180812173710_copy_status_stats.rb +++ b/db/migrate/20180812173710_copy_status_stats.rb @@ -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 diff --git a/db/migrate/20181116173541_copy_account_stats.rb b/db/migrate/20181116173541_copy_account_stats.rb index 88dc0c1fe9..30d07764ef 100644 --- a/db/migrate/20181116173541_copy_account_stats.rb +++ b/db/migrate/20181116173541_copy_account_stats.rb @@ -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 diff --git a/lib/mastodon/cli/statuses.rb b/lib/mastodon/cli/statuses.rb index bd5b047077..0d6018a2b9 100644 --- a/lib/mastodon/cli/statuses.rb +++ b/lib/mastodon/cli/statuses.rb @@ -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)