diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 5f2297381a..8f05dcab3e 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -28,11 +28,7 @@ jobs:
env:
RAILS_ENV: ${{ matrix.mode }}
BUNDLE_WITH: ${{ matrix.mode }}
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY: precompile_placeholder
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT: precompile_placeholder
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY: precompile_placeholder
- OTP_SECRET: precompile_placeholder
- SECRET_KEY_BASE: precompile_placeholder
+ SECRET_KEY_BASE_DUMMY: 1
steps:
- uses: actions/checkout@v4
diff --git a/Dockerfile b/Dockerfile
index cb5b872059..2dc7602b2d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -212,11 +212,7 @@ ARG TARGETPLATFORM
RUN \
# Use Ruby on Rails to create Mastodon assets
- ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=precompile_placeholder \
- ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=precompile_placeholder \
- ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=precompile_placeholder \
- OTP_SECRET=precompile_placeholder \
- SECRET_KEY_BASE=precompile_placeholder \
+ SECRET_KEY_BASE_DUMMY=1 \
bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp;
diff --git a/Gemfile.lock b/Gemfile.lock
index 4d16fea47c..c2253fe19c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -109,7 +109,7 @@ GEM
aws-sdk-kms (1.83.0)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sigv4 (~> 1.1)
- aws-sdk-s3 (1.152.0)
+ aws-sdk-s3 (1.152.1)
aws-sdk-core (~> 3, >= 3.197.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8)
@@ -498,6 +498,10 @@ GEM
opentelemetry-semantic_conventions
opentelemetry-helpers-sql-obfuscation (0.1.0)
opentelemetry-common (~> 0.20)
+ opentelemetry-instrumentation-action_mailer (0.1.0)
+ opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-active_support (~> 0.1)
+ opentelemetry-instrumentation-base (~> 0.22.1)
opentelemetry-instrumentation-action_pack (0.9.0)
opentelemetry-api (~> 1.0)
opentelemetry-instrumentation-base (~> 0.22.1)
@@ -551,8 +555,9 @@ GEM
opentelemetry-api (~> 1.0)
opentelemetry-common (~> 0.20.0)
opentelemetry-instrumentation-base (~> 0.22.1)
- opentelemetry-instrumentation-rails (0.30.1)
+ opentelemetry-instrumentation-rails (0.30.2)
opentelemetry-api (~> 1.0)
+ opentelemetry-instrumentation-action_mailer (~> 0.1.0)
opentelemetry-instrumentation-action_pack (~> 0.9.0)
opentelemetry-instrumentation-action_view (~> 0.7.0)
opentelemetry-instrumentation-active_job (~> 0.7.0)
diff --git a/app/controllers/admin/domain_blocks_controller.rb b/app/controllers/admin/domain_blocks_controller.rb
index 325b33df80..16a8cb9eea 100644
--- a/app/controllers/admin/domain_blocks_controller.rb
+++ b/app/controllers/admin/domain_blocks_controller.rb
@@ -4,6 +4,18 @@ module Admin
class DomainBlocksController < BaseController
before_action :set_domain_block, only: [:destroy, :edit, :update]
+ PERMITTED_PARAMS = %i(
+ domain
+ obfuscate
+ private_comment
+ public_comment
+ reject_media
+ reject_reports
+ severity
+ ).freeze
+
+ PERMITTED_UPDATE_PARAMS = PERMITTED_PARAMS.without(:domain).freeze
+
def batch
authorize :domain_block, :create?
@form = Form::DomainBlockBatch.new(form_domain_block_batch_params.merge(current_account: current_account, action: action_from_button))
@@ -88,11 +100,17 @@ module Admin
end
def update_params
- params.require(:domain_block).permit(:severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
+ params
+ .require(:domain_block)
+ .slice(*PERMITTED_UPDATE_PARAMS)
+ .permit(*PERMITTED_UPDATE_PARAMS)
end
def resource_params
- params.require(:domain_block).permit(:domain, :severity, :reject_media, :reject_reports, :private_comment, :public_comment, :obfuscate)
+ params
+ .require(:domain_block)
+ .slice(*PERMITTED_PARAMS)
+ .permit(*PERMITTED_PARAMS)
end
def form_domain_block_batch_params
diff --git a/app/controllers/api/v1/admin/tags_controller.rb b/app/controllers/api/v1/admin/tags_controller.rb
index 67d987d0e3..283383acb4 100644
--- a/app/controllers/api/v1/admin/tags_controller.rb
+++ b/app/controllers/api/v1/admin/tags_controller.rb
@@ -13,6 +13,13 @@ class Api::V1::Admin::TagsController < Api::BaseController
LIMIT = 100
+ PERMITTED_PARAMS = %i(
+ display_name
+ listable
+ trendable
+ usable
+ ).freeze
+
def index
authorize :tag, :index?
render json: @tags, each_serializer: REST::Admin::TagSerializer
@@ -40,7 +47,9 @@ class Api::V1::Admin::TagsController < Api::BaseController
end
def tag_params
- params.permit(:display_name, :trendable, :usable, :listable)
+ params
+ .slice(*PERMITTED_PARAMS)
+ .permit(*PERMITTED_PARAMS)
end
def next_path
diff --git a/app/javascript/mastodon/locales/ar.json b/app/javascript/mastodon/locales/ar.json
index 68e32dd2aa..6d67d354b6 100644
--- a/app/javascript/mastodon/locales/ar.json
+++ b/app/javascript/mastodon/locales/ar.json
@@ -225,7 +225,11 @@
"domain_pill.their_username": "مُعرّفُهم الفريد على الخادم. من الممكن العثور على مستخدمين بنفس اسم المستخدم على خوادم مختلفة.",
"domain_pill.username": "اسم المستخدم",
"domain_pill.whats_in_a_handle": "ما المقصود بالمُعرِّف؟",
+ "domain_pill.who_they_are": "بما أن المعالجات تقول من هو الشخص ومكان وجوده، يمكنك التفاعل مع الناس عبر الشبكة الاجتماعية لـ .",
+ "domain_pill.who_you_are": "لأن معالجتك تقول من أنت ومكان وجودك، يمكن الناس التفاعل معك عبر الشبكة الاجتماعية لـ .",
"domain_pill.your_handle": "عنوانك الكامل:",
+ "domain_pill.your_server": "منزلك الرقمي، حيث تعيش جميع مشاركاتك. لا تحب هذا؟ إنقل الخوادم في أي وقت واخضر متابعينك أيضًا.",
+ "domain_pill.your_username": "معرفك الفريد على هذا الخادم. من الممكن العثور على مستخدمين بنفس إسم المستخدم على خوادم مختلفة.",
"embed.instructions": "يمكنكم إدماج هذا المنشور على موقعكم الإلكتروني عن طريق نسخ الشفرة أدناه.",
"embed.preview": "إليك ما سيبدو عليه:",
"emoji_button.activity": "الأنشطة",
@@ -262,6 +266,7 @@
"empty_column.list": "هذه القائمة فارغة مؤقتا و لكن سوف تمتلئ تدريجيا عندما يبدأ الأعضاء المُنتَمين إليها بنشر منشورات.",
"empty_column.lists": "ليس عندك أية قائمة بعد. سوف تظهر قوائمك هنا إن قمت بإنشاء واحدة.",
"empty_column.mutes": "لم تقم بكتم أي مستخدم بعد.",
+ "empty_column.notification_requests": "لا يوجد شيء هنا. عندما تتلقى إشعارات جديدة، سوف تظهر هنا وفقًا لإعداداتك.",
"empty_column.notifications": "لم تتلق أي إشعار بعدُ. تفاعل مع المستخدمين الآخرين لإنشاء محادثة.",
"empty_column.public": "لا يوجد أي شيء هنا! قم بنشر شيء ما للعامة، أو اتبع المستخدمين الآخرين المتواجدين على الخوادم الأخرى لملء خيط المحادثات",
"error.unexpected_crash.explanation": "نظرا لوجود خطأ في التعليمات البرمجية أو مشكلة توافق مع المتصفّح، تعذر عرض هذه الصفحة بشكل صحيح.",
@@ -292,6 +297,8 @@
"filter_modal.select_filter.subtitle": "استخدم فئة موجودة أو قم بإنشاء فئة جديدة",
"filter_modal.select_filter.title": "تصفية هذا المنشور",
"filter_modal.title.status": "تصفية منشور",
+ "filtered_notifications_banner.mentions": "{count, plural, one {إشارة} two {إشارتين} few {# إشارات} other {# إشارة}}",
+ "filtered_notifications_banner.pending_requests": "إشعارات من {count, plural, zero {}=0 {لا أحد} one {شخص واحد قد تعرفه} two {شخصين قد تعرفهما} few {# أشخاص قد تعرفهم} many {# شخص قد تعرفهم} other {# شخص قد تعرفهم}}",
"filtered_notifications_banner.title": "الإشعارات المصفاة",
"firehose.all": "الكل",
"firehose.local": "هذا الخادم",
@@ -301,6 +308,8 @@
"follow_requests.unlocked_explanation": "حتى وإن كان حسابك غير مقفل، يعتقد فريق {domain} أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدوياً.",
"follow_suggestions.curated_suggestion": "اختيار الموظفين",
"follow_suggestions.dismiss": "لا تُظهرها مجدّدًا",
+ "follow_suggestions.featured_longer": "مختار يدوياً من قِبل فريق {domain}",
+ "follow_suggestions.friends_of_friends_longer": "مشهور بين الأشخاص الذين تتابعهم",
"follow_suggestions.hints.featured": "تم اختيار هذا الملف الشخصي يدوياً من قبل فريق {domain}.",
"follow_suggestions.hints.friends_of_friends": "هذا الملف الشخصي مشهور بين الأشخاص الذين تتابعهم.",
"follow_suggestions.hints.most_followed": "هذا الملف الشخصي هو واحد من الأكثر متابعة على {domain}.",
@@ -405,6 +414,7 @@
"limited_account_hint.action": "إظهار الملف التعريفي على أي حال",
"limited_account_hint.title": "تم إخفاء هذا الملف الشخصي من قبل مشرفي {domain}.",
"link_preview.author": "مِن {name}",
+ "link_preview.more_from_author": "المزيد من {name}",
"lists.account.add": "أضف إلى القائمة",
"lists.account.remove": "احذف من القائمة",
"lists.delete": "احذف القائمة",
@@ -465,10 +475,13 @@
"notification.follow_request": "لقد طلب {name} متابعتك",
"notification.mention": "{name} ذكرك",
"notification.moderation-warning.learn_more": "اعرف المزيد",
+ "notification.moderation_warning": "لقد تلقيت تحذيرًا بالإشراف",
+ "notification.moderation_warning.action_delete_statuses": "تم إزالة بعض مشاركاتك.",
"notification.moderation_warning.action_disable": "تم تعطيل حسابك.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "بعض من منشوراتك تم تصنيفها على أنها حساسة.",
"notification.moderation_warning.action_none": "لقد تلقى حسابك تحذيرا بالإشراف.",
"notification.moderation_warning.action_sensitive": "سيتم وضع علامة على منشوراتك على أنها حساسة من الآن فصاعدا.",
+ "notification.moderation_warning.action_silence": "لقد تم تقييد حسابك.",
"notification.moderation_warning.action_suspend": "لقد تم تعليق حسابك.",
"notification.own_poll": "انتهى استطلاعك للرأي",
"notification.poll": "لقد انتهى استطلاع رأي شاركتَ فيه",
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index f6ec44fb53..26bb2bee14 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -613,9 +613,10 @@ code {
font-family: inherit;
pointer-events: none;
cursor: default;
- max-width: 140px;
+ max-width: 50%;
white-space: nowrap;
overflow: hidden;
+ text-overflow: ellipsis;
&::after {
content: '';
diff --git a/app/lib/access_grant_extension.rb b/app/lib/access_grant_extension.rb
new file mode 100644
index 0000000000..bf8f5ae25a
--- /dev/null
+++ b/app/lib/access_grant_extension.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module AccessGrantExtension
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') }
+ scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) }
+ end
+end
diff --git a/app/lib/access_token_extension.rb b/app/lib/access_token_extension.rb
index 4e9585dd1e..6e06f988a5 100644
--- a/app/lib/access_token_extension.rb
+++ b/app/lib/access_token_extension.rb
@@ -9,6 +9,10 @@ module AccessTokenExtension
has_many :web_push_subscriptions, class_name: 'Web::PushSubscription', inverse_of: :access_token
after_commit :push_to_streaming_api
+
+ scope :expired, -> { where.not(expires_in: nil).where('created_at + MAKE_INTERVAL(secs => expires_in) < NOW()') }
+ scope :not_revoked, -> { where(revoked_at: nil) }
+ scope :revoked, -> { where.not(revoked_at: nil).where(revoked_at: ...Time.now.utc) }
end
def revoke(clock = Time)
diff --git a/app/lib/admin/metrics/measure/instance_accounts_measure.rb b/app/lib/admin/metrics/measure/instance_accounts_measure.rb
index 3d081fdd90..746780ee77 100644
--- a/app/lib/admin/metrics/measure/instance_accounts_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_accounts_measure.rb
@@ -43,7 +43,7 @@ class Admin::Metrics::Measure::InstanceAccountsMeasure < Admin::Metrics::Measure
SELECT count(*) FROM new_accounts
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/instance_followers_measure.rb b/app/lib/admin/metrics/measure/instance_followers_measure.rb
index 378c6754d9..0693d5a64a 100644
--- a/app/lib/admin/metrics/measure/instance_followers_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_followers_measure.rb
@@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceFollowersMeasure < Admin::Metrics::Measur
SELECT count(*) FROM new_followers
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/instance_follows_measure.rb b/app/lib/admin/metrics/measure/instance_follows_measure.rb
index e213348fbc..90d3819358 100644
--- a/app/lib/admin/metrics/measure/instance_follows_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_follows_measure.rb
@@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceFollowsMeasure < Admin::Metrics::Measure:
SELECT count(*) FROM new_follows
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb
index 1d2dbbe414..89f8b41497 100644
--- a/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_media_attachments_measure.rb
@@ -53,7 +53,7 @@ class Admin::Metrics::Measure::InstanceMediaAttachmentsMeasure < Admin::Metrics:
SELECT COALESCE(SUM(size), 0) FROM new_media_attachments
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/instance_reports_measure.rb b/app/lib/admin/metrics/measure/instance_reports_measure.rb
index 9da3d53e34..5f58387a64 100644
--- a/app/lib/admin/metrics/measure/instance_reports_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_reports_measure.rb
@@ -44,7 +44,7 @@ class Admin::Metrics::Measure::InstanceReportsMeasure < Admin::Metrics::Measure:
SELECT count(*) FROM new_reports
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/instance_statuses_measure.rb b/app/lib/admin/metrics/measure/instance_statuses_measure.rb
index b918a30a57..5873c6e71f 100644
--- a/app/lib/admin/metrics/measure/instance_statuses_measure.rb
+++ b/app/lib/admin/metrics/measure/instance_statuses_measure.rb
@@ -45,7 +45,7 @@ class Admin::Metrics::Measure::InstanceStatusesMeasure < Admin::Metrics::Measure
SELECT count(*) FROM new_statuses
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/new_users_measure.rb b/app/lib/admin/metrics/measure/new_users_measure.rb
index 6837c14c82..32057154d6 100644
--- a/app/lib/admin/metrics/measure/new_users_measure.rb
+++ b/app/lib/admin/metrics/measure/new_users_measure.rb
@@ -32,7 +32,7 @@ class Admin::Metrics::Measure::NewUsersMeasure < Admin::Metrics::Measure::BaseMe
SELECT count(*) FROM new_users
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/opened_reports_measure.rb b/app/lib/admin/metrics/measure/opened_reports_measure.rb
index c395c46341..47de38bbe6 100644
--- a/app/lib/admin/metrics/measure/opened_reports_measure.rb
+++ b/app/lib/admin/metrics/measure/opened_reports_measure.rb
@@ -32,7 +32,7 @@ class Admin::Metrics::Measure::OpenedReportsMeasure < Admin::Metrics::Measure::B
SELECT count(*) FROM new_reports
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/query_helper.rb b/app/lib/admin/metrics/measure/query_helper.rb
index 969065f73f..5969a96ba9 100644
--- a/app/lib/admin/metrics/measure/query_helper.rb
+++ b/app/lib/admin/metrics/measure/query_helper.rb
@@ -15,6 +15,14 @@ module Admin::Metrics::Measure::QueryHelper
ActiveRecord::Base.sanitize_sql_array(sql_array)
end
+ def generated_series_days
+ Arel.sql(
+ <<~SQL.squish
+ SELECT generate_series(timestamp :start_at, :end_at, '1 day')::date AS period
+ SQL
+ )
+ end
+
def account_domain_sql(include_subdomains)
if include_subdomains
"accounts.domain IN (SELECT domain FROM instances WHERE reverse('.' || domain) LIKE reverse('.' || :domain::text))"
diff --git a/app/lib/admin/metrics/measure/resolved_reports_measure.rb b/app/lib/admin/metrics/measure/resolved_reports_measure.rb
index 780db75a10..ecfd779c86 100644
--- a/app/lib/admin/metrics/measure/resolved_reports_measure.rb
+++ b/app/lib/admin/metrics/measure/resolved_reports_measure.rb
@@ -32,7 +32,7 @@ class Admin::Metrics::Measure::ResolvedReportsMeasure < Admin::Metrics::Measure:
SELECT count(*) FROM resolved_reports
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) AS axis
SQL
end
diff --git a/app/lib/admin/metrics/measure/tag_servers_measure.rb b/app/lib/admin/metrics/measure/tag_servers_measure.rb
index f273d739d0..5db1076062 100644
--- a/app/lib/admin/metrics/measure/tag_servers_measure.rb
+++ b/app/lib/admin/metrics/measure/tag_servers_measure.rb
@@ -40,7 +40,7 @@ class Admin::Metrics::Measure::TagServersMeasure < Admin::Metrics::Measure::Base
SELECT COUNT(*) FROM tag_servers
) AS value
FROM (
- SELECT generate_series(date_trunc('day', :start_at::timestamp)::date, date_trunc('day', :end_at::timestamp)::date, interval '1 day') AS period
+ #{generated_series_days}
) as axis
SQL
end
diff --git a/app/lib/vacuum/access_tokens_vacuum.rb b/app/lib/vacuum/access_tokens_vacuum.rb
index a224f6d638..281ae22bf0 100644
--- a/app/lib/vacuum/access_tokens_vacuum.rb
+++ b/app/lib/vacuum/access_tokens_vacuum.rb
@@ -9,12 +9,12 @@ class Vacuum::AccessTokensVacuum
private
def vacuum_revoked_access_tokens!
- Doorkeeper::AccessToken.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all
- Doorkeeper::AccessToken.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all
+ Doorkeeper::AccessToken.expired.in_batches.delete_all
+ Doorkeeper::AccessToken.revoked.in_batches.delete_all
end
def vacuum_revoked_access_grants!
- Doorkeeper::AccessGrant.where.not(expires_in: nil).where('created_at + make_interval(secs => expires_in) < NOW()').in_batches.delete_all
- Doorkeeper::AccessGrant.where.not(revoked_at: nil).where('revoked_at < NOW()').in_batches.delete_all
+ Doorkeeper::AccessGrant.expired.in_batches.delete_all
+ Doorkeeper::AccessGrant.revoked.in_batches.delete_all
end
end
diff --git a/app/models/web/push_subscription.rb b/app/models/web/push_subscription.rb
index b482ad3afe..ddfd08146e 100644
--- a/app/models/web/push_subscription.rb
+++ b/app/models/web/push_subscription.rb
@@ -75,7 +75,7 @@ class Web::PushSubscription < ApplicationRecord
class << self
def unsubscribe_for(application_id, resource_owner)
- access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id, revoked_at: nil).pluck(:id)
+ access_token_ids = Doorkeeper::AccessToken.where(application_id: application_id, resource_owner_id: resource_owner.id).not_revoked.pluck(:id)
where(access_token_id: access_token_ids).delete_all
end
end
diff --git a/app/models/webauthn_credential.rb b/app/models/webauthn_credential.rb
index 4fa31ece52..d7ed1b9d40 100644
--- a/app/models/webauthn_credential.rb
+++ b/app/models/webauthn_credential.rb
@@ -15,9 +15,11 @@
#
class WebauthnCredential < ApplicationRecord
+ SIGN_COUNT_LIMIT = (2**63)
+
validates :external_id, :public_key, :nickname, :sign_count, presence: true
validates :external_id, uniqueness: true
validates :nickname, uniqueness: { scope: :user_id }
validates :sign_count,
- numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: (2**63) - 1 }
+ numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than_or_equal_to: SIGN_COUNT_LIMIT - 1 }
end
diff --git a/app/views/redirects/show.html.haml b/app/views/redirects/show.html.haml
index 0d09387a9c..64436e05d1 100644
--- a/app/views/redirects/show.html.haml
+++ b/app/views/redirects/show.html.haml
@@ -1,3 +1,7 @@
+- content_for :header_tags do
+ %meta{ name: 'robots', content: 'noindex, noarchive' }/
+ %link{ rel: 'canonical', href: @redirect_path }
+
.redirect
.redirect__logo
= link_to render_logo, root_path
diff --git a/config/application.rb b/config/application.rb
index b3a9b99ff5..65407da05c 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -115,6 +115,7 @@ module Mastodon
Doorkeeper::AuthorizationsController.layout 'modal'
Doorkeeper::AuthorizedApplicationsController.layout 'admin'
Doorkeeper::Application.include ApplicationExtension
+ Doorkeeper::AccessGrant.include AccessGrantExtension
Doorkeeper::AccessToken.include AccessTokenExtension
Devise::FailureApp.include AbstractController::Callbacks
Devise::FailureApp.include Localized
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 0ce4618e16..534ec4667d 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -157,7 +157,11 @@ Rails.application.configure do
}
# TODO: Remove once devise-two-factor data migration complete
- config.x.otp_secret = ENV.fetch('OTP_SECRET')
+ config.x.otp_secret = if ENV['SECRET_KEY_BASE_DUMMY']
+ SecureRandom.hex(64)
+ else
+ ENV.fetch('OTP_SECRET')
+ end
# Enable DNS rebinding protection and other `Host` header attacks.
# config.hosts = [
diff --git a/config/initializers/active_record_encryption.rb b/config/initializers/active_record_encryption.rb
index 900f3c68f0..a83ca80765 100644
--- a/config/initializers/active_record_encryption.rb
+++ b/config/initializers/active_record_encryption.rb
@@ -5,6 +5,11 @@
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY
).each do |key|
+ if ENV['SECRET_KEY_BASE_DUMMY']
+ # Use placeholder value during production env asset compilation
+ ENV[key] = SecureRandom.hex(64)
+ end
+
value = ENV.fetch(key) do
abort <<~MESSAGE
diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb
index 0f168d233b..378af0961d 100644
--- a/lib/paperclip/color_extractor.rb
+++ b/lib/paperclip/color_extractor.rb
@@ -122,26 +122,28 @@ module Paperclip
colors['out_array'].zip(colors['x_array'], colors['y_array']).map do |v, x, y|
rgb_from_xyv(histogram, x, y, v)
- end.reverse
+ end.flatten.reverse.uniq
end
# rubocop:disable Naming/MethodParameterName
def rgb_from_xyv(image, x, y, v)
pixel = image.getpoint(x, y)
- # Unfortunately, we only have the first 2 dimensions, so try to
- # guess the third one by looking up the value
+ # As we only have the first 2 dimensions for this maximum, we
+ # can't distinguish with different maxima with the same `r` and `g`
+ # values but different `b` values.
+ #
+ # Therefore, we return an array of maxima, which is always non-empty,
+ # but may contain multiple colors with the same values.
- # NOTE: this means that if multiple bins with the same `r` and `g`
- # components have the same number of occurrences, we will always return
- # the one with the lowest `b` value. This means that in case of a tie,
- # we will return the same color twice and skip the ones it tied with.
- z = pixel.find_index(v)
+ pixel.filter_map.with_index do |pv, z|
+ next if pv != v
- r = (x + 0.5) * 256 / BINS
- g = (y + 0.5) * 256 / BINS
- b = (z + 0.5) * 256 / BINS
- ColorDiff::Color::RGB.new(r, g, b)
+ r = (x + 0.5) * 256 / BINS
+ g = (y + 0.5) * 256 / BINS
+ b = (z + 0.5) * 256 / BINS
+ ColorDiff::Color::RGB.new(r, g, b)
+ end
end
def w3c_contrast(color1, color2)
diff --git a/spec/controllers/settings/preferences/appearance_controller_spec.rb b/spec/controllers/settings/preferences/appearance_controller_spec.rb
index 261c426acb..c59d315104 100644
--- a/spec/controllers/settings/preferences/appearance_controller_spec.rb
+++ b/spec/controllers/settings/preferences/appearance_controller_spec.rb
@@ -23,8 +23,11 @@ describe Settings::Preferences::AppearanceController do
end
describe 'PUT #update' do
+ subject { put :update, params: { user: { settings_attributes: { skin: 'contrast' } } } }
+
it 'redirects correctly' do
- put :update, params: { user: { setting_theme: 'contrast' } }
+ expect { subject }
+ .to change { user.reload.settings.skin }.to('contrast')
expect(response).to redirect_to(settings_preferences_appearance_path)
end
diff --git a/spec/models/account_domain_block_spec.rb b/spec/models/account_domain_block_spec.rb
index 10bd579363..d994403b8e 100644
--- a/spec/models/account_domain_block_spec.rb
+++ b/spec/models/account_domain_block_spec.rb
@@ -3,22 +3,30 @@
require 'rails_helper'
RSpec.describe AccountDomainBlock do
+ let(:account) { Fabricate(:account) }
+
it 'removes blocking cache after creation' do
- account = Fabricate(:account)
Rails.cache.write("exclude_domains_for:#{account.id}", 'a.domain.already.blocked')
- described_class.create!(account: account, domain: 'a.domain.blocked.later')
-
- expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to be false
+ expect { block_domain_for_account('a.domain.blocked.later') }
+ .to change { account_has_exclude_domains_cache? }.to(false)
end
it 'removes blocking cache after destruction' do
- account = Fabricate(:account)
- block = described_class.create!(account: account, domain: 'domain')
+ block = block_domain_for_account('domain')
Rails.cache.write("exclude_domains_for:#{account.id}", 'domain')
- block.destroy!
+ expect { block.destroy! }
+ .to change { account_has_exclude_domains_cache? }.to(false)
+ end
- expect(Rails.cache.exist?("exclude_domains_for:#{account.id}")).to be false
+ private
+
+ def block_domain_for_account(domain)
+ Fabricate(:account_domain_block, account: account, domain: domain)
+ end
+
+ def account_has_exclude_domains_cache?
+ Rails.cache.exist?("exclude_domains_for:#{account.id}")
end
end
diff --git a/spec/models/follow_spec.rb b/spec/models/follow_spec.rb
index c7743183cc..9aa172b2f2 100644
--- a/spec/models/follow_spec.rb
+++ b/spec/models/follow_spec.rb
@@ -36,16 +36,15 @@ RSpec.describe Follow do
end
end
- describe 'recent' do
- it 'sorts so that more recent follows comes earlier' do
- follow0 = described_class.create!(account: alice, target_account: bob)
- follow1 = described_class.create!(account: bob, target_account: alice)
+ describe '.recent' do
+ let!(:follow_earlier) { Fabricate(:follow) }
+ let!(:follow_later) { Fabricate(:follow) }
- a = described_class.recent.to_a
+ it 'sorts with most recent follows first' do
+ results = described_class.recent
- expect(a.size).to eq 2
- expect(a[0]).to eq follow1
- expect(a[1]).to eq follow0
+ expect(results.size).to eq 2
+ expect(results).to eq [follow_later, follow_earlier]
end
end
diff --git a/spec/models/import_spec.rb b/spec/models/import_spec.rb
index 3605f0b9bf..10df5f8c0b 100644
--- a/spec/models/import_spec.rb
+++ b/spec/models/import_spec.rb
@@ -8,11 +8,6 @@ RSpec.describe Import do
let(:data) { attachment_fixture('imports.txt') }
describe 'validations' do
- it 'has a valid parameters' do
- import = described_class.create(account: account, type: type, data: data)
- expect(import).to be_valid
- end
-
it 'is invalid without an type' do
import = described_class.create(account: account, data: data)
expect(import).to model_have_error_on_field(:type)
diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb
index 5aa5548cc8..ebcc459078 100644
--- a/spec/models/poll_spec.rb
+++ b/spec/models/poll_spec.rb
@@ -31,14 +31,6 @@ describe Poll do
end
describe 'validations' do
- context 'when valid' do
- let(:poll) { Fabricate.build(:poll) }
-
- it 'is valid with valid attributes' do
- expect(poll).to be_valid
- end
- end
-
context 'when not valid' do
let(:poll) { Fabricate.build(:poll, expires_at: nil) }
diff --git a/spec/models/webauthn_credentials_spec.rb b/spec/models/webauthn_credential_spec.rb
similarity index 95%
rename from spec/models/webauthn_credentials_spec.rb
rename to spec/models/webauthn_credential_spec.rb
index 9631245e11..23f0229a67 100644
--- a/spec/models/webauthn_credentials_spec.rb
+++ b/spec/models/webauthn_credential_spec.rb
@@ -71,8 +71,8 @@ RSpec.describe WebauthnCredential do
expect(webauthn_credential).to model_have_error_on_field(:sign_count)
end
- it 'is invalid if sign_count is greater 2**63 - 1' do
- webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: 2**63)
+ it 'is invalid if sign_count is greater than the limit' do
+ webauthn_credential = Fabricate.build(:webauthn_credential, sign_count: (described_class::SIGN_COUNT_LIMIT * 2))
webauthn_credential.valid?
diff --git a/spec/requests/api/v1/admin/domain_allows_spec.rb b/spec/requests/api/v1/admin/domain_allows_spec.rb
index 662a8f9a8d..b8f0b0055c 100644
--- a/spec/requests/api/v1/admin/domain_allows_spec.rb
+++ b/spec/requests/api/v1/admin/domain_allows_spec.rb
@@ -113,7 +113,7 @@ RSpec.describe 'Domain Allows' do
end
context 'with invalid domain name' do
- let(:params) { 'foo bar' }
+ let(:params) { { domain: 'foo bar' } }
it 'returns http unprocessable entity' do
subject