From 5c0a9aac3b2add81ff2a9c7e724dff4eb9c6451b Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Sep 2023 15:09:44 +0200 Subject: [PATCH 1/7] Revert to using primary database in IndexingScheduler (#26754) --- app/workers/scheduler/indexing_scheduler.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/workers/scheduler/indexing_scheduler.rb b/app/workers/scheduler/indexing_scheduler.rb index 1b09730c7d..ff1b744442 100644 --- a/app/workers/scheduler/indexing_scheduler.rb +++ b/app/workers/scheduler/indexing_scheduler.rb @@ -16,9 +16,7 @@ class Scheduler::IndexingScheduler indexes.each do |type| with_redis do |redis| redis.sscan_each("chewy:queue:#{type.name}", count: SCAN_BATCH_SIZE).each_slice(IMPORT_BATCH_SIZE) do |ids| - with_read_replica do - type.import!(ids) - end + type.import!(ids) redis.srem("chewy:queue:#{type.name}", ids) end From 6c4c72497a5722870e4432ef41dd4c9ec36a8928 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Sep 2023 15:13:27 +0200 Subject: [PATCH 2/7] Fix search popout including full-text search instructions when full-text search is disabled (#26755) --- .../features/compose/components/search.jsx | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/search.jsx b/app/javascript/mastodon/features/compose/components/search.jsx index 1c629bcbb4..848b812632 100644 --- a/app/javascript/mastodon/features/compose/components/search.jsx +++ b/app/javascript/mastodon/features/compose/components/search.jsx @@ -80,7 +80,7 @@ class Search extends PureComponent { handleKeyDown = (e) => { const { selectedOption } = this.state; - const options = this._getOptions().concat(this.defaultOptions); + const options = searchEnabled ? this._getOptions().concat(this.defaultOptions) : this._getOptions(); switch(e.key) { case 'Escape': @@ -353,15 +353,19 @@ class Search extends PureComponent { )} -

+ {searchEnabled && ( + <> +

-
- {this.defaultOptions.map(({ key, label, action }, i) => ( - - ))} -
+
+ {this.defaultOptions.map(({ key, label, action }, i) => ( + + ))} +
+ + )} ); From 9e26cd55038084638fdf71b75f526494777d2849 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Sep 2023 15:41:10 +0200 Subject: [PATCH 3/7] Add `authorized_fetch` server setting in addition to env var (#25798) --- app/controllers/application_controller.rb | 5 +---- app/helpers/authorized_fetch_helper.rb | 11 +++++++++++ app/javascript/styles/mastodon/accounts.scss | 2 ++ app/javascript/styles/mastodon/forms.scss | 1 + app/models/form/admin_settings.rb | 10 ++++++++++ app/services/concerns/payloadable.rb | 4 +++- app/views/admin/settings/discovery/show.html.haml | 5 +++++ config/i18n-tasks.yml | 2 +- config/initializers/simple_form.rb | 5 +++-- config/locales/en.yml | 5 +++++ config/locales/simple_form.en.yml | 1 + 11 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 app/helpers/authorized_fetch_helper.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 975315e247..6ec93f824e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base include CacheConcern include DomainControlHelper include DatabaseHelper + include AuthorizedFetchHelper helper_method :current_account helper_method :current_session @@ -51,10 +52,6 @@ class ApplicationController < ActionController::Base private - def authorized_fetch_mode? - ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.limited_federation_mode - end - def public_fetch_mode? !authorized_fetch_mode? end diff --git a/app/helpers/authorized_fetch_helper.rb b/app/helpers/authorized_fetch_helper.rb new file mode 100644 index 0000000000..ce87526e6a --- /dev/null +++ b/app/helpers/authorized_fetch_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module AuthorizedFetchHelper + def authorized_fetch_mode? + ENV.fetch('AUTHORIZED_FETCH') { Setting.authorized_fetch } == 'true' || Rails.configuration.x.limited_federation_mode + end + + def authorized_fetch_overridden? + ENV.key?('AUTHORIZED_FETCH') || Rails.configuration.x.limited_federation_mode + end +end diff --git a/app/javascript/styles/mastodon/accounts.scss b/app/javascript/styles/mastodon/accounts.scss index 2a5285ee02..80d6c13cef 100644 --- a/app/javascript/styles/mastodon/accounts.scss +++ b/app/javascript/styles/mastodon/accounts.scss @@ -188,6 +188,7 @@ } .information-badge, +.simple_form .overridden, .simple_form .recommended, .simple_form .not_recommended { display: inline-block; @@ -204,6 +205,7 @@ } .information-badge, +.simple_form .overridden, .simple_form .recommended, .simple_form .not_recommended { background-color: rgba($ui-secondary-color, 0.1); diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss index a7079c1457..0f8eecee01 100644 --- a/app/javascript/styles/mastodon/forms.scss +++ b/app/javascript/styles/mastodon/forms.scss @@ -103,6 +103,7 @@ code { } } + .overridden, .recommended, .not_recommended { position: absolute; diff --git a/app/models/form/admin_settings.rb b/app/models/form/admin_settings.rb index a6be55fd7b..7be026d85f 100644 --- a/app/models/form/admin_settings.rb +++ b/app/models/form/admin_settings.rb @@ -3,6 +3,8 @@ class Form::AdminSettings include ActiveModel::Model + include AuthorizedFetchHelper + KEYS = %i( site_contact_username site_contact_email @@ -34,6 +36,7 @@ class Form::AdminSettings backups_retention_period status_page_url captcha_enabled + authorized_fetch ).freeze INTEGER_KEYS = %i( @@ -54,6 +57,7 @@ class Form::AdminSettings noindex require_invite_text captcha_enabled + authorized_fetch ).freeze UPLOAD_KEYS = %i( @@ -61,6 +65,10 @@ class Form::AdminSettings mascot ).freeze + OVERRIDEN_SETTINGS = { + authorized_fetch: :authorized_fetch_mode?, + }.freeze + attr_accessor(*KEYS) validates :registrations_mode, inclusion: { in: %w(open approved none) }, if: -> { defined?(@registrations_mode) } @@ -80,6 +88,8 @@ class Form::AdminSettings stored_value = if UPLOAD_KEYS.include?(key) SiteUpload.where(var: key).first_or_initialize(var: key) + elsif OVERRIDEN_SETTINGS.include?(key) + public_send(OVERRIDEN_SETTINGS[key]) else Setting.public_send(key) end diff --git a/app/services/concerns/payloadable.rb b/app/services/concerns/payloadable.rb index 1389a42ed6..bd9d9d74b5 100644 --- a/app/services/concerns/payloadable.rb +++ b/app/services/concerns/payloadable.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module Payloadable + include AuthorizedFetchHelper + # @param [ActiveModelSerializers::Model] record # @param [ActiveModelSerializers::Serializer] serializer # @param [Hash] options @@ -23,6 +25,6 @@ module Payloadable end def signing_enabled? - ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.limited_federation_mode + !authorized_fetch_mode? end end diff --git a/app/views/admin/settings/discovery/show.html.haml b/app/views/admin/settings/discovery/show.html.haml index c48e0fdc62..62011d5c56 100644 --- a/app/views/admin/settings/discovery/show.html.haml +++ b/app/views/admin/settings/discovery/show.html.haml @@ -39,6 +39,11 @@ .fields-group = f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, recommended: :recommended + %h4= t('admin.settings.security.federation_authentication') + + .fields-group + = f.input :authorized_fetch, as: :boolean, wrapper: :with_label, label: t('admin.settings.security.authorized_fetch'), warning_hint: authorized_fetch_overridden? ? t('admin.settings.security.authorized_fetch_overridden_hint') : nil, hint: t('admin.settings.security.authorized_fetch_hint'), disabled: authorized_fetch_overridden?, recommended: authorized_fetch_overridden? ? :overridden : nil + %h4= t('admin.settings.discovery.follow_recommendations') .fields-group diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index d0677b80fb..2d4487ce56 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -50,7 +50,7 @@ ignore_unused: - 'activerecord.errors.*' - '{devise,pagination,doorkeeper}.*' - '{date,datetime,time,number}.*' - - 'simple_form.{yes,no,recommended,not_recommended}' + - 'simple_form.{yes,no,recommended,not_recommended,overridden}' - 'simple_form.{placeholders,hints,labels}.*' - 'simple_form.{error_notification,required}.:' - 'errors.messages.*' diff --git a/config/initializers/simple_form.rb b/config/initializers/simple_form.rb index 9d90cc6ca8..a7a2d251e5 100644 --- a/config/initializers/simple_form.rb +++ b/config/initializers/simple_form.rb @@ -97,7 +97,8 @@ SimpleForm.setup do |config| end end - b.use :hint, wrap_with: { tag: :span, class: :hint } + b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] } + b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :error, wrap_with: { tag: :span, class: :error } end @@ -111,8 +112,8 @@ SimpleForm.setup do |config| config.wrappers :with_block_label, class: [:input, :with_block_label], hint_class: :field_with_hint, error_class: :field_with_errors do |b| b.use :html5 b.use :label - b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :warning_hint, wrap_with: { tag: :span, class: [:hint, 'warning-hint'] } + b.use :hint, wrap_with: { tag: :span, class: :hint } b.use :input, wrap_with: { tag: :div, class: :label_input } b.use :error, wrap_with: { tag: :span, class: :error } end diff --git a/config/locales/en.yml b/config/locales/en.yml index 8bdfd1ec91..693155d6ef 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -770,6 +770,11 @@ en: approved: Approval required for sign up none: Nobody can sign up open: Anyone can sign up + security: + authorized_fetch: Require authentication from federated servers + authorized_fetch_hint: Requiring authentication from federated servers enables stricter enforcement of both user-level and server-level blocks. However, this comes at the cost of a performance penalty, reduces the reach of your replies, and may introduce compatibility issues with some federated services. In addition, this will not prevent dedicated actors from fetching your public posts and accounts. + authorized_fetch_overridden_hint: You are currently unable to change this setting because it is overridden by an environment variable. + federation_authentication: Federation authentication enforcement title: Server settings site_uploads: delete: Delete uploaded file diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index efda7b778b..b1297606bc 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -317,6 +317,7 @@ en: url: Endpoint URL 'no': 'No' not_recommended: Not recommended + overridden: Overridden recommended: Recommended required: mark: "*" From be991f1d18006a4820c1e9ca6625bf2bd2bfedac Mon Sep 17 00:00:00 2001 From: Gabriel Simmer Date: Fri, 1 Sep 2023 16:44:28 +0100 Subject: [PATCH 4/7] Move to ioredis for streaming (#26581) Co-authored-by: Emelia Smith --- package.json | 2 +- streaming/index.js | 82 +++++++++++++++++------------------- yarn.lock | 101 +++++++++++++++++++++------------------------ 3 files changed, 85 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 4038831050..e92115610b 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "immutable": "^4.3.0", "imports-loader": "^1.2.0", "intl-messageformat": "^10.3.5", + "ioredis": "^5.3.2", "js-yaml": "^4.1.0", "jsdom": "^22.1.0", "lodash": "^4.17.21", @@ -118,7 +119,6 @@ "react-swipeable-views": "^0.14.0", "react-textarea-autosize": "^8.4.1", "react-toggle": "^4.1.3", - "redis": "^4.6.5", "redux": "^4.2.1", "redux-immutable": "^4.0.0", "redux-thunk": "^2.4.2", diff --git a/streaming/index.js b/streaming/index.js index a241fa3280..c9fac063df 100644 --- a/streaming/index.js +++ b/streaming/index.js @@ -6,12 +6,12 @@ const url = require('url'); const dotenv = require('dotenv'); const express = require('express'); +const Redis = require('ioredis'); const { JSDOM } = require('jsdom'); const log = require('npmlog'); const pg = require('pg'); const dbUrlToConfig = require('pg-connection-string').parse; const metrics = require('prom-client'); -const redis = require('redis'); const uuid = require('uuid'); const WebSocket = require('ws'); @@ -24,30 +24,12 @@ dotenv.config({ log.level = process.env.LOG_LEVEL || 'verbose'; /** - * @param {Object.} defaultConfig - * @param {string} redisUrl + * @param {Object.} config */ -const redisUrlToClient = async (defaultConfig, redisUrl) => { - const config = defaultConfig; - - let client; - - if (!redisUrl) { - client = redis.createClient(config); - } else if (redisUrl.startsWith('unix://')) { - client = redis.createClient(Object.assign(config, { - socket: { - path: redisUrl.slice(7), - }, - })); - } else { - client = redis.createClient(Object.assign(config, { - url: redisUrl, - })); - } - +const createRedisClient = async (config) => { + const { redisParams, redisUrl } = config; + const client = new Redis(redisUrl, redisParams); client.on('error', (err) => log.error('Redis Client Error!', err)); - await client.connect(); return client; }; @@ -147,23 +129,22 @@ const pgConfigFromEnv = (env) => { * @returns {Object.} configuration for the Redis connection */ const redisConfigFromEnv = (env) => { - const redisNamespace = env.REDIS_NAMESPACE || null; + // ioredis *can* transparently add prefixes for us, but it doesn't *in some cases*, + // which means we can't use it. But this is something that should be looked into. + const redisPrefix = env.REDIS_NAMESPACE ? `${env.REDIS_NAMESPACE}:` : ''; const redisParams = { - socket: { - host: env.REDIS_HOST || '127.0.0.1', - port: env.REDIS_PORT || 6379, - }, - database: env.REDIS_DB || 0, + host: env.REDIS_HOST || '127.0.0.1', + port: env.REDIS_PORT || 6379, + db: env.REDIS_DB || 0, password: env.REDIS_PASSWORD || undefined, }; - if (redisNamespace) { - redisParams.namespace = redisNamespace; + // redisParams.path takes precedence over host and port. + if (env.REDIS_URL && env.REDIS_URL.startsWith('unix://')) { + redisParams.path = env.REDIS_URL.slice(7); } - const redisPrefix = redisNamespace ? `${redisNamespace}:` : ''; - return { redisParams, redisPrefix, @@ -179,15 +160,15 @@ const startServer = async () => { const pgPool = new pg.Pool(pgConfigFromEnv(process.env)); const server = http.createServer(app); - const { redisParams, redisUrl, redisPrefix } = redisConfigFromEnv(process.env); - /** * @type {Object.): void>>} */ const subs = {}; - const redisSubscribeClient = await redisUrlToClient(redisParams, redisUrl); - const redisClient = await redisUrlToClient(redisParams, redisUrl); + const redisConfig = redisConfigFromEnv(process.env); + const redisSubscribeClient = await createRedisClient(redisConfig); + const redisClient = await createRedisClient(redisConfig); + const { redisPrefix } = redisConfig; // Collect metrics from Node.js metrics.collectDefaultMetrics(); @@ -277,13 +258,13 @@ const startServer = async () => { }; /** - * @param {string} message * @param {string} channel + * @param {string} message */ - const onRedisMessage = (message, channel) => { + const onRedisMessage = (channel, message) => { const callbacks = subs[channel]; - log.silly(`New message on channel ${channel}`); + log.silly(`New message on channel ${redisPrefix}${channel}`); if (!callbacks) { return; @@ -294,6 +275,7 @@ const startServer = async () => { callbacks.forEach(callback => callback(json)); }; + redisSubscribeClient.on("message", onRedisMessage); /** * @callback SubscriptionListener @@ -312,8 +294,14 @@ const startServer = async () => { if (subs[channel].length === 0) { log.verbose(`Subscribe ${channel}`); - redisSubscribeClient.subscribe(channel, onRedisMessage); - redisSubscriptions.inc(); + redisSubscribeClient.subscribe(channel, (err, count) => { + if (err) { + log.error(`Error subscribing to ${channel}`); + } + else { + redisSubscriptions.set(count); + } + }); } subs[channel].push(callback); @@ -334,8 +322,14 @@ const startServer = async () => { if (subs[channel].length === 0) { log.verbose(`Unsubscribe ${channel}`); - redisSubscribeClient.unsubscribe(channel); - redisSubscriptions.dec(); + redisSubscribeClient.unsubscribe(channel, (err, count) => { + if (err) { + log.error(`Error unsubscribing to ${channel}`); + } + else { + redisSubscriptions.set(count); + } + }); delete subs[channel]; } }; diff --git a/yarn.lock b/yarn.lock index 880ded9773..74ed5214e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1452,6 +1452,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -1786,40 +1791,6 @@ resolved "https://registry.yarnpkg.com/@rails/ujs/-/ujs-7.0.7.tgz#54af8d66160a8a7bf7d8f184703d2bf4b3fab914" integrity sha512-J2v5Ca7HgejO7diGKiDylaVDQKmbQ5FJih6Oo3hXuBKEuXlcaccJu64lj8MNVLaPVyZx0g4gaOQZQz95QEb/hg== -"@redis/bloom@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.2.0.tgz#d3fd6d3c0af3ef92f26767b56414a370c7b63b71" - integrity sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg== - -"@redis/client@1.5.9": - version "1.5.9" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.5.9.tgz#c4ee81bbfedb4f1d9c7c5e9859661b9388fb4021" - integrity sha512-SffgN+P1zdWJWSXBvJeynvEnmnZrYmtKSRW00xl8pOPFOMJjxRR9u0frSxJpPR6Y4V+k54blJjGW7FgxbTI7bQ== - dependencies: - cluster-key-slot "1.1.2" - generic-pool "3.9.0" - yallist "4.0.0" - -"@redis/graph@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.1.0.tgz#cc2b82e5141a29ada2cce7d267a6b74baa6dd519" - integrity sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg== - -"@redis/json@1.0.4": - version "1.0.4" - resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1" - integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw== - -"@redis/search@1.1.3": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.1.3.tgz#b5a6837522ce9028267fe6f50762a8bcfd2e998b" - integrity sha512-4Dg1JjvCevdiCBTZqjhKkGoC5/BcB7k9j99kdMnaXFXg8x4eyOIVg9487CMv7/BUVkFLZCaIh8ead9mU15DNng== - -"@redis/time-series@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.5.tgz#a6d70ef7a0e71e083ea09b967df0a0ed742bc6ad" - integrity sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg== - "@reduxjs/toolkit@^1.9.5": version "1.9.5" resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.9.5.tgz#d3987849c24189ca483baa7aa59386c8e52077c4" @@ -4111,7 +4082,7 @@ clone-deep@^4.0.1: kind-of "^6.0.2" shallow-clone "^3.0.0" -cluster-key-slot@1.1.2: +cluster-key-slot@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== @@ -4857,6 +4828,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -6139,11 +6115,6 @@ gauge@^5.0.0: strip-ansi "^6.0.1" wide-align "^1.1.5" -generic-pool@3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.9.0.tgz#36f4a678e963f4fdb8707eab050823abc4e8f5e4" - integrity sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g== - gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -6823,6 +6794,21 @@ invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" +ioredis@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.2.tgz#9139f596f62fc9c72d873353ac5395bcf05709f7" + integrity sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -10283,17 +10269,17 @@ redent@^4.0.0: indent-string "^5.0.0" strip-indent "^4.0.0" -redis@^4.6.5: - version "4.6.8" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.6.8.tgz#54c5992e8a5ba512506fe9f53142cadc405547e7" - integrity sha512-S7qNkPUYrsofQ0ztWlTHSaK0Qqfl1y+WMIxrzeAGNG+9iUZB4HGeBgkHxE6uJJ6iXrkvLd1RVJ2nvu6H1sAzfQ== +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== dependencies: - "@redis/bloom" "1.2.0" - "@redis/client" "1.5.9" - "@redis/graph" "1.1.0" - "@redis/json" "1.0.4" - "@redis/search" "1.1.3" - "@redis/time-series" "1.0.5" + redis-errors "^1.0.0" redux-immutable@^4.0.0: version "4.0.0" @@ -11211,6 +11197,11 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -12966,16 +12957,16 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@4.0.0, yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + yaml@^1.10.0: version "1.10.2" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" From 16681e0f20e1f8584e11439953c8d59b322571f5 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 1 Sep 2023 17:47:07 +0200 Subject: [PATCH 5/7] Add admin notifications for new Mastodon versions (#26582) --- .../admin/software_updates_controller.rb | 18 ++ .../components/critical_update_banner.tsx | 26 +++ .../mastodon/features/home_timeline/index.jsx | 14 +- app/javascript/mastodon/initial_state.js | 2 + app/javascript/mastodon/locales/en.json | 3 + app/javascript/styles/mastodon/admin.scss | 5 + .../styles/mastodon/components.scss | 18 +- app/javascript/styles/mastodon/tables.scss | 5 + app/lib/admin/system_check.rb | 1 + .../system_check/software_version_check.rb | 27 +++ app/mailers/admin_mailer.rb | 16 ++ app/models/software_update.rb | 40 +++++ app/models/user_settings.rb | 1 + app/policies/software_update_policy.rb | 7 + app/presenters/initial_state_presenter.rb | 6 +- app/serializers/initial_state_serializer.rb | 2 + app/services/software_update_check_service.rb | 82 +++++++++ .../admin/software_updates/index.html.haml | 29 ++++ .../new_critical_software_updates.text.erb | 5 + .../new_software_updates.text.erb | 5 + .../preferences/notifications/show.html.haml | 6 +- .../software_update_check_scheduler.rb | 11 ++ config/locales/en.yml | 25 +++ config/locales/simple_form.en.yml | 6 + config/navigation.rb | 3 + config/routes/admin.rb | 2 + config/sidekiq.yml | 4 + .../20230822081029_create_software_updates.rb | 16 ++ db/schema.rb | 12 +- lib/mastodon/version.rb | 4 + lib/tasks/mastodon.rake | 4 + .../fabricators/software_update_fabricator.rb | 7 + spec/features/admin/software_updates_spec.rb | 23 +++ .../software_version_check_spec.rb | 133 +++++++++++++++ spec/mailers/admin_mailer_spec.rb | 42 +++++ spec/models/software_update_spec.rb | 87 ++++++++++ spec/policies/software_update_policy_spec.rb | 25 +++ .../software_update_check_service_spec.rb | 158 ++++++++++++++++++ .../software_update_check_scheduler_spec.rb | 20 +++ 39 files changed, 892 insertions(+), 8 deletions(-) create mode 100644 app/controllers/admin/software_updates_controller.rb create mode 100644 app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx create mode 100644 app/lib/admin/system_check/software_version_check.rb create mode 100644 app/models/software_update.rb create mode 100644 app/policies/software_update_policy.rb create mode 100644 app/services/software_update_check_service.rb create mode 100644 app/views/admin/software_updates/index.html.haml create mode 100644 app/views/admin_mailer/new_critical_software_updates.text.erb create mode 100644 app/views/admin_mailer/new_software_updates.text.erb create mode 100644 app/workers/scheduler/software_update_check_scheduler.rb create mode 100644 db/migrate/20230822081029_create_software_updates.rb create mode 100644 spec/fabricators/software_update_fabricator.rb create mode 100644 spec/features/admin/software_updates_spec.rb create mode 100644 spec/lib/admin/system_check/software_version_check_spec.rb create mode 100644 spec/models/software_update_spec.rb create mode 100644 spec/policies/software_update_policy_spec.rb create mode 100644 spec/services/software_update_check_service_spec.rb create mode 100644 spec/workers/scheduler/software_update_check_scheduler_spec.rb diff --git a/app/controllers/admin/software_updates_controller.rb b/app/controllers/admin/software_updates_controller.rb new file mode 100644 index 0000000000..52d8cb41e6 --- /dev/null +++ b/app/controllers/admin/software_updates_controller.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Admin + class SoftwareUpdatesController < BaseController + before_action :check_enabled! + + def index + authorize :software_update, :index? + @software_updates = SoftwareUpdate.all.sort_by(&:gem_version) + end + + private + + def check_enabled! + not_found unless SoftwareUpdate.check_enabled? + end + end +end diff --git a/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx new file mode 100644 index 0000000000..d0dd2b6acd --- /dev/null +++ b/app/javascript/mastodon/features/home_timeline/components/critical_update_banner.tsx @@ -0,0 +1,26 @@ +import { FormattedMessage } from 'react-intl'; + +export const CriticalUpdateBanner = () => ( +
+
+

+ +

+

+ {' '} + + + +

+
+
+); diff --git a/app/javascript/mastodon/features/home_timeline/index.jsx b/app/javascript/mastodon/features/home_timeline/index.jsx index 1cd6edd7aa..8ff0377946 100644 --- a/app/javascript/mastodon/features/home_timeline/index.jsx +++ b/app/javascript/mastodon/features/home_timeline/index.jsx @@ -14,7 +14,7 @@ import { fetchAnnouncements, toggleShowAnnouncements } from 'mastodon/actions/an import { IconWithBadge } from 'mastodon/components/icon_with_badge'; import { NotSignedInIndicator } from 'mastodon/components/not_signed_in_indicator'; import AnnouncementsContainer from 'mastodon/features/getting_started/containers/announcements_container'; -import { me } from 'mastodon/initial_state'; +import { me, criticalUpdatesPending } from 'mastodon/initial_state'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { expandHomeTimeline } from '../../actions/timelines'; @@ -23,6 +23,7 @@ import ColumnHeader from '../../components/column_header'; import StatusListContainer from '../ui/containers/status_list_container'; import { ColumnSettings } from './components/column_settings'; +import { CriticalUpdateBanner } from './components/critical_update_banner'; import { ExplorePrompt } from './components/explore_prompt'; const messages = defineMessages({ @@ -156,8 +157,9 @@ class HomeTimeline extends PureComponent { const { intl, hasUnread, columnId, multiColumn, tooSlow, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props; const pinned = !!columnId; const { signedIn } = this.context.identity; + const banners = []; - let announcementsButton, banner; + let announcementsButton; if (hasAnnouncements) { announcementsButton = ( @@ -173,8 +175,12 @@ class HomeTimeline extends PureComponent { ); } + if (criticalUpdatesPending) { + banners.push(); + } + if (tooSlow) { - banner = ; + banners.push(); } return ( @@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent { {signedIn ? ( } accounts * @property {InitialStateLanguage[]} languages + * @property {boolean=} critical_updates_pending * @property {InitialStateMeta} meta */ @@ -140,6 +141,7 @@ export const useBlurhash = getMeta('use_blurhash'); export const usePendingItems = getMeta('use_pending_items'); export const version = getMeta('version'); export const languages = initialState?.languages; +export const criticalUpdatesPending = initialState?.critical_updates_pending; // @ts-expect-error export const statusPageUrl = getMeta('status_page_url'); export const sso_redirect = getMeta('sso_redirect'); diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 90bb9616f0..13cddba723 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -310,6 +310,9 @@ "home.explore_prompt.body": "Your home feed will have a mix of posts from the hashtags you've chosen to follow, the people you've chosen to follow, and the posts they boost. If that feels too quiet, you may want to:", "home.explore_prompt.title": "This is your home base within Mastodon.", "home.hide_announcements": "Hide announcements", + "home.pending_critical_update.body": "Please update your Mastodon server as soon as possible!", + "home.pending_critical_update.link": "See updates", + "home.pending_critical_update.title": "Critical security update available!", "home.show_announcements": "Show announcements", "interaction_modal.description.favourite": "With an account on Mastodon, you can favorite this post to let the author know you appreciate it and save it for later.", "interaction_modal.description.follow": "With an account on Mastodon, you can follow {name} to receive their posts in your home feed.", diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index bbb6ffdff7..a65f35e7b1 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -143,6 +143,11 @@ $content-width: 840px; } } + .warning a { + color: $gold-star; + font-weight: 700; + } + .simple-navigation-active-leaf a { color: $primary-text-color; background-color: $ui-highlight-color; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index f61cd059fe..10083a2a32 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -8860,7 +8860,8 @@ noscript { } } -.dismissable-banner { +.dismissable-banner, +.warning-banner { position: relative; margin: 10px; margin-bottom: 5px; @@ -8938,6 +8939,21 @@ noscript { } } +.warning-banner { + border: 1px solid $warning-red; + background: rgba($warning-red, 0.15); + + &__message { + h1 { + color: $warning-red; + } + + a { + color: $primary-text-color; + } + } +} + .image { position: relative; overflow: hidden; diff --git a/app/javascript/styles/mastodon/tables.scss b/app/javascript/styles/mastodon/tables.scss index 38cfc87271..dd5b483ec4 100644 --- a/app/javascript/styles/mastodon/tables.scss +++ b/app/javascript/styles/mastodon/tables.scss @@ -12,6 +12,11 @@ border-top: 1px solid $ui-base-color; text-align: start; background: darken($ui-base-color, 4%); + + &.critical { + font-weight: 700; + color: $gold-star; + } } & > thead > tr > th { diff --git a/app/lib/admin/system_check.rb b/app/lib/admin/system_check.rb index 89dfcef9f1..25c88341a4 100644 --- a/app/lib/admin/system_check.rb +++ b/app/lib/admin/system_check.rb @@ -2,6 +2,7 @@ class Admin::SystemCheck ACTIVE_CHECKS = [ + Admin::SystemCheck::SoftwareVersionCheck, Admin::SystemCheck::MediaPrivacyCheck, Admin::SystemCheck::DatabaseSchemaCheck, Admin::SystemCheck::SidekiqProcessCheck, diff --git a/app/lib/admin/system_check/software_version_check.rb b/app/lib/admin/system_check/software_version_check.rb new file mode 100644 index 0000000000..e142feddf0 --- /dev/null +++ b/app/lib/admin/system_check/software_version_check.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Admin::SystemCheck::SoftwareVersionCheck < Admin::SystemCheck::BaseCheck + include RoutingHelper + + def skip? + !current_user.can?(:view_devops) || !SoftwareUpdate.check_enabled? + end + + def pass? + software_updates.empty? + end + + def message + if software_updates.any?(&:urgent?) + Admin::SystemCheck::Message.new(:software_version_critical_check, nil, admin_software_updates_path, true) + else + Admin::SystemCheck::Message.new(:software_version_patch_check, nil, admin_software_updates_path) + end + end + + private + + def software_updates + @software_updates ||= SoftwareUpdate.pending_to_a.filter { |update| update.urgent? || update.patch_type? } + end +end diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 5baf9b38a5..990b92c337 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -45,6 +45,22 @@ class AdminMailer < ApplicationMailer end end + def new_software_updates + locale_for_account(@me) do + mail subject: default_i18n_subject(instance: @instance) + end + end + + def new_critical_software_updates + headers['Priority'] = 'urgent' + headers['X-Priority'] = '1' + headers['Importance'] = 'high' + + locale_for_account(@me) do + mail subject: default_i18n_subject(instance: @instance) + end + end + private def process_params diff --git a/app/models/software_update.rb b/app/models/software_update.rb new file mode 100644 index 0000000000..cb3a6df2ae --- /dev/null +++ b/app/models/software_update.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: software_updates +# +# id :bigint(8) not null, primary key +# version :string not null +# urgent :boolean default(FALSE), not null +# type :integer default("patch"), not null +# release_notes :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# + +class SoftwareUpdate < ApplicationRecord + self.inheritance_column = nil + + enum type: { patch: 0, minor: 1, major: 2 }, _suffix: :type + + def gem_version + Gem::Version.new(version) + end + + class << self + def check_enabled? + ENV['UPDATE_CHECK_URL'] != '' + end + + def pending_to_a + return [] unless check_enabled? + + all.to_a.filter { |update| update.gem_version > Mastodon::Version.gem_version } + end + + def urgent_pending? + pending_to_a.any?(&:urgent?) + end + end +end diff --git a/app/models/user_settings.rb b/app/models/user_settings.rb index 678467c75d..030cbec4d8 100644 --- a/app/models/user_settings.rb +++ b/app/models/user_settings.rb @@ -44,6 +44,7 @@ class UserSettings setting :pending_account, default: true setting :trends, default: true setting :appeal, default: true + setting :software_updates, default: 'critical', in: %w(none critical patch all) end namespace :interactions do diff --git a/app/policies/software_update_policy.rb b/app/policies/software_update_policy.rb new file mode 100644 index 0000000000..dcb565814f --- /dev/null +++ b/app/policies/software_update_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SoftwareUpdatePolicy < ApplicationPolicy + def index? + role.can?(:view_devops) + end +end diff --git a/app/presenters/initial_state_presenter.rb b/app/presenters/initial_state_presenter.rb index b87cff51e1..222cc8566c 100644 --- a/app/presenters/initial_state_presenter.rb +++ b/app/presenters/initial_state_presenter.rb @@ -3,9 +3,13 @@ class InitialStatePresenter < ActiveModelSerializers::Model attributes :settings, :push_subscription, :token, :current_account, :admin, :owner, :text, :visibility, - :disabled_account, :moved_to_account + :disabled_account, :moved_to_account, :critical_updates_pending def role current_account&.user_role end + + def critical_updates_pending + role&.can?(:view_devops) && SoftwareUpdate.urgent_pending? + end end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 9660c941d0..56d45c588e 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -7,6 +7,8 @@ class InitialStateSerializer < ActiveModel::Serializer :media_attachments, :settings, :languages + attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? } + has_one :push_subscription, serializer: REST::WebPushSubscriptionSerializer has_one :role, serializer: REST::RoleSerializer diff --git a/app/services/software_update_check_service.rb b/app/services/software_update_check_service.rb new file mode 100644 index 0000000000..49b92f104d --- /dev/null +++ b/app/services/software_update_check_service.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +class SoftwareUpdateCheckService < BaseService + def call + clean_outdated_updates! + return unless SoftwareUpdate.check_enabled? + + process_update_notices!(fetch_update_notices) + end + + private + + def clean_outdated_updates! + SoftwareUpdate.find_each do |software_update| + software_update.delete if Mastodon::Version.gem_version >= software_update.gem_version + rescue ArgumentError + software_update.delete + end + end + + def fetch_update_notices + Request.new(:get, "#{api_url}?version=#{version}").add_headers('Accept' => 'application/json', 'User-Agent' => 'Mastodon update checker').perform do |res| + return Oj.load(res.body_with_limit, mode: :strict) if res.code == 200 + end + rescue HTTP::Error, OpenSSL::SSL::SSLError, Oj::ParseError + nil + end + + def api_url + ENV.fetch('UPDATE_CHECK_URL', 'https://api.joinmastodon.org/update-check') + end + + def version + @version ||= Mastodon::Version.to_s.split('+')[0] + end + + def process_update_notices!(update_notices) + return if update_notices.blank? || update_notices['updatesAvailable'].blank? + + # Clear notices that are not listed by the update server anymore + SoftwareUpdate.where.not(version: update_notices['updatesAvailable'].pluck('version')).delete_all + + # Check if any of the notices is new, and issue notifications + known_versions = SoftwareUpdate.where(version: update_notices['updatesAvailable'].pluck('version')).pluck(:version) + new_update_notices = update_notices['updatesAvailable'].filter { |notice| known_versions.exclude?(notice['version']) } + return if new_update_notices.blank? + + new_updates = new_update_notices.map do |notice| + SoftwareUpdate.create!(version: notice['version'], urgent: notice['urgent'], type: notice['type'], release_notes: notice['releaseNotes']) + end + + notify_devops!(new_updates) + end + + def should_notify_user?(user, urgent_version, patch_version) + case user.settings['notification_emails.software_updates'] + when 'none' + false + when 'critical' + urgent_version + when 'patch' + urgent_version || patch_version + when 'all' + true + end + end + + def notify_devops!(new_updates) + has_new_urgent_version = new_updates.any?(&:urgent?) + has_new_patch_version = new_updates.any?(&:patch_type?) + + User.those_who_can(:view_devops).includes(:account).find_each do |user| + next unless should_notify_user?(user, has_new_urgent_version, has_new_patch_version) + + if has_new_urgent_version + AdminMailer.with(recipient: user.account).new_critical_software_updates.deliver_later + else + AdminMailer.with(recipient: user.account).new_software_updates.deliver_later + end + end + end +end diff --git a/app/views/admin/software_updates/index.html.haml b/app/views/admin/software_updates/index.html.haml new file mode 100644 index 0000000000..7a223ee07b --- /dev/null +++ b/app/views/admin/software_updates/index.html.haml @@ -0,0 +1,29 @@ +- content_for :page_title do + = t('admin.software_updates.title') + +.simple_form + %p.lead + = t('admin.software_updates.description') + = link_to t('admin.software_updates.documentation_link'), 'https://docs.joinmastodon.org/admin/upgrading/#automated_checks', target: '_new' + +%hr.spacer + +- unless @software_updates.empty? + .table-wrapper + %table.table + %thead + %tr + %th= t('admin.software_updates.version') + %th= t('admin.software_updates.type') + %th + %th + %tbody + - @software_updates.each do |update| + %tr + %td= update.version + %td= t("admin.software_updates.types.#{update.type}") + - if update.urgent? + %td.critical= t("admin.software_updates.critical_update") + - else + %td + %td= table_link_to 'link', t('admin.software_updates.release_notes'), update.release_notes diff --git a/app/views/admin_mailer/new_critical_software_updates.text.erb b/app/views/admin_mailer/new_critical_software_updates.text.erb new file mode 100644 index 0000000000..c901bc50f7 --- /dev/null +++ b/app/views/admin_mailer/new_critical_software_updates.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_critical_software_updates.body') %> + +<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %> diff --git a/app/views/admin_mailer/new_software_updates.text.erb b/app/views/admin_mailer/new_software_updates.text.erb new file mode 100644 index 0000000000..2fc4d1a5f2 --- /dev/null +++ b/app/views/admin_mailer/new_software_updates.text.erb @@ -0,0 +1,5 @@ +<%= raw t('application_mailer.salutation', name: display_name(@me)) %> + +<%= raw t('admin_mailer.new_software_updates.body') %> + +<%= raw t('application_mailer.view')%> <%= admin_software_updates_url %> diff --git a/app/views/settings/preferences/notifications/show.html.haml b/app/views/settings/preferences/notifications/show.html.haml index 0913bda9ae..5cc101069c 100644 --- a/app/views/settings/preferences/notifications/show.html.haml +++ b/app/views/settings/preferences/notifications/show.html.haml @@ -22,7 +22,7 @@ .fields-group = ff.input :always_send_emails, wrapper: :with_label, label: I18n.t('simple_form.labels.defaults.setting_always_send_emails'), hint: I18n.t('simple_form.hints.defaults.setting_always_send_emails') - - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) + - if current_user.can?(:manage_reports, :manage_appeals, :manage_users, :manage_taxonomies) || (SoftwareUpdate.check_enabled? && current_user.can?(:view_devops)) %h4= t 'notifications.administration_emails' .fields-group @@ -31,6 +31,10 @@ = ff.input :'notification_emails.pending_account', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.pending_account') if current_user.can?(:manage_users) = ff.input :'notification_emails.trends', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.trending_tag') if current_user.can?(:manage_taxonomies) + - if SoftwareUpdate.check_enabled? && current_user.can?(:view_devops) + .fields-group + = ff.input :'notification_emails.software_updates', wrapper: :with_label, label: I18n.t('simple_form.labels.notification_emails.software_updates.label'), collection: %w(none critical patch all), label_method: ->(setting) { I18n.t("simple_form.labels.notification_emails.software_updates.#{setting}") }, include_blank: false, hint: false + %h4= t 'notifications.other_settings' .fields-group diff --git a/app/workers/scheduler/software_update_check_scheduler.rb b/app/workers/scheduler/software_update_check_scheduler.rb new file mode 100644 index 0000000000..c732bdedc0 --- /dev/null +++ b/app/workers/scheduler/software_update_check_scheduler.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class Scheduler::SoftwareUpdateCheckScheduler + include Sidekiq::Worker + + sidekiq_options retry: 0, lock: :until_executed, lock_ttl: 1.hour.to_i + + def perform + SoftwareUpdateCheckService.new.call + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 693155d6ef..71e5fb843e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -309,6 +309,7 @@ en: unpublish: Unpublish unpublished_msg: Announcement successfully unpublished! updated_msg: Announcement successfully updated! + critical_update_pending: Critical update pending custom_emojis: assign_category: Assign category by_domain: Domain @@ -779,6 +780,18 @@ en: site_uploads: delete: Delete uploaded file destroyed_msg: Site upload successfully deleted! + software_updates: + critical_update: Critical — please update quickly + description: It is recommended to keep your Mastodon installation up to date to benefit from the latest fixes and features. Moreover, it is sometimes critical to update Mastodon in a timely manner to avoid security issues. For these reasons, Mastodon checks for updates every 30 minutes, and will notify you according to your e-mail notification preferences. + documentation_link: Learn more + release_notes: Release notes + title: Available updates + type: Type + types: + major: Major release + minor: Minor release + patch: Patch release — bugfixes and easy to apply changes + version: Version statuses: account: Author application: Application @@ -843,6 +856,12 @@ en: message_html: You haven't defined any server rules. sidekiq_process_check: message_html: No Sidekiq process running for the %{value} queue(s). Please review your Sidekiq configuration + software_version_critical_check: + action: See available updates + message_html: A critical Mastodon update is available, please update as quickly as possible. + software_version_patch_check: + action: See available updates + message_html: A bugfix Mastodon update is available. upload_check_privacy_error: action: Check here for more information message_html: "Your web server is misconfigured. The privacy of your users is at risk." @@ -956,6 +975,9 @@ en: body: "%{target} is appealing a moderation decision by %{action_taken_by} from %{date}, which was %{type}. They wrote:" next_steps: You can approve the appeal to undo the moderation decision, or ignore it. subject: "%{username} is appealing a moderation decision on %{instance}" + new_critical_software_updates: + body: New critical versions of Mastodon have been released, you may want to update as soon as possible! + subject: Critical Mastodon updates are available for %{instance}! new_pending_account: body: The details of the new account are below. You can approve or reject this application. subject: New account up for review on %{instance} (%{username}) @@ -963,6 +985,9 @@ en: body: "%{reporter} has reported %{target}" body_remote: Someone from %{domain} has reported %{target} subject: New report for %{instance} (#%{id}) + new_software_updates: + body: New Mastodon versions have been released, you may want to update! + subject: New Mastodon versions are available for %{instance}! new_trends: body: 'The following items need a review before they can be displayed publicly:' new_trending_links: diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index b1297606bc..0b718c5b65 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -291,6 +291,12 @@ en: pending_account: New account needs review reblog: Someone boosted your post report: New report is submitted + software_updates: + all: Notify on all updates + critical: Notify on critical updates only + label: A new Mastodon version is available + none: Never notify of updates (not recommended) + patch: Notify on bugfix updates trending_tag: New trend requires review rule: text: Rule diff --git a/config/navigation.rb b/config/navigation.rb index f608c2eea7..e86c695a98 100644 --- a/config/navigation.rb +++ b/config/navigation.rb @@ -3,6 +3,9 @@ SimpleNavigation::Configuration.run do |navigation| navigation.items do |n| n.item :web, safe_join([fa_icon('chevron-left fw'), t('settings.back')]), root_path + + n.item :software_updates, safe_join([fa_icon('exclamation-circle fw'), t('admin.critical_update_pending')]), admin_software_updates_path, if: -> { ENV['UPDATE_CHECK_URL'] != '' && current_user.can?(:view_devops) && SoftwareUpdate.urgent_pending? }, html: { class: 'warning' } + n.item :profile, safe_join([fa_icon('user fw'), t('settings.profile')]), settings_profile_path, if: -> { current_user.functional? }, highlights_on: %r{/settings/profile|/settings/featured_tags|/settings/verification|/settings/privacy} n.item :preferences, safe_join([fa_icon('cog fw'), t('settings.preferences')]), settings_preferences_path, if: -> { current_user.functional? } do |s| diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 4573878ede..207cb0580d 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -201,4 +201,6 @@ namespace :admin do end end end + + resources :software_updates, only: [:index] end diff --git a/config/sidekiq.yml b/config/sidekiq.yml index 12c45c22a1..f1ba5651dd 100644 --- a/config/sidekiq.yml +++ b/config/sidekiq.yml @@ -58,3 +58,7 @@ interval: 1 minute class: Scheduler::SuspendedUserCleanupScheduler queue: scheduler + software_update_check_scheduler: + interval: 30 minutes + class: Scheduler::SoftwareUpdateCheckScheduler + queue: scheduler diff --git a/db/migrate/20230822081029_create_software_updates.rb b/db/migrate/20230822081029_create_software_updates.rb new file mode 100644 index 0000000000..146d5d3037 --- /dev/null +++ b/db/migrate/20230822081029_create_software_updates.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class CreateSoftwareUpdates < ActiveRecord::Migration[7.0] + def change + create_table :software_updates do |t| + t.string :version, null: false + t.boolean :urgent, default: false, null: false + t.integer :type, default: 0, null: false + t.string :release_notes, default: '', null: false + + t.timestamps + end + + add_index :software_updates, :version, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 8b758fc7df..c861069420 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do +ActiveRecord::Schema[7.0].define(version: 2023_08_22_081029) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -903,6 +903,16 @@ ActiveRecord::Schema[7.0].define(version: 2023_08_18_142253) do t.index ["var"], name: "index_site_uploads_on_var", unique: true end + create_table "software_updates", force: :cascade do |t| + t.string "version", null: false + t.boolean "urgent", default: false, null: false + t.integer "type", default: 0, null: false + t.string "release_notes", default: "", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["version"], name: "index_software_updates_on_version", unique: true + end + create_table "status_edits", force: :cascade do |t| t.bigint "status_id", null: false t.bigint "account_id" diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index c542d5d49a..65f90f93fd 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -39,6 +39,10 @@ module Mastodon components.join end + def gem_version + @gem_version ||= Gem::Version.new(to_s.split('+')[0]) + end + def repository ENV.fetch('GITHUB_REPOSITORY', 'mastodon/mastodon') end diff --git a/lib/tasks/mastodon.rake b/lib/tasks/mastodon.rake index 010caaf8ea..f68d1cf1f8 100644 --- a/lib/tasks/mastodon.rake +++ b/lib/tasks/mastodon.rake @@ -424,6 +424,10 @@ namespace :mastodon do end end + prompt.say "\n" + + env['UPDATE_CHECK_URL'] = '' unless prompt.yes?('Do you want Mastodon to periodically check for important updates and notify you? (Recommended)', default: true) + prompt.say "\n" prompt.say 'This configuration will be written to .env.production' diff --git a/spec/fabricators/software_update_fabricator.rb b/spec/fabricators/software_update_fabricator.rb new file mode 100644 index 0000000000..622fff66e8 --- /dev/null +++ b/spec/fabricators/software_update_fabricator.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +Fabricator(:software_update) do + version '99.99.99' + urgent false + type 'patch' +end diff --git a/spec/features/admin/software_updates_spec.rb b/spec/features/admin/software_updates_spec.rb new file mode 100644 index 0000000000..4a635d1a79 --- /dev/null +++ b/spec/features/admin/software_updates_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'finding software updates through the admin interface' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true, release_notes: 'https://github.com/mastodon/mastodon/releases/v99') + + sign_in Fabricate(:user, role: UserRole.find_by(name: 'Owner')), scope: :user + end + + it 'shows a link to the software updates page, which links to release notes' do + visit settings_profile_path + click_on I18n.t('admin.critical_update_pending') + + expect(page).to have_title(I18n.t('admin.software_updates.title')) + + expect(page).to have_content('99.99.99') + + click_on I18n.t('admin.software_updates.release_notes') + expect(page).to have_current_path('https://github.com/mastodon/mastodon/releases/v99', url: true) + end +end diff --git a/spec/lib/admin/system_check/software_version_check_spec.rb b/spec/lib/admin/system_check/software_version_check_spec.rb new file mode 100644 index 0000000000..de4335fc51 --- /dev/null +++ b/spec/lib/admin/system_check/software_version_check_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Admin::SystemCheck::SoftwareVersionCheck do + include RoutingHelper + + subject(:check) { described_class.new(user) } + + let(:user) { Fabricate(:user) } + + describe 'skip?' do + context 'when user cannot view devops' do + before { allow(user).to receive(:can?).with(:view_devops).and_return(false) } + + it 'returns true' do + expect(check.skip?).to be true + end + end + + context 'when user can view devops' do + before { allow(user).to receive(:can?).with(:view_devops).and_return(true) } + + it 'returns false' do + expect(check.skip?).to be false + end + + context 'when checks are disabled' do + around do |example| + ClimateControl.modify UPDATE_CHECK_URL: '' do + example.run + end + end + + it 'returns true' do + expect(check.skip?).to be true + end + end + end + end + + describe 'pass?' do + context 'when there is no known update' do + it 'returns true' do + expect(check.pass?).to be true + end + end + + context 'when there is a non-urgent major release' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: false) + end + + it 'returns true' do + expect(check.pass?).to be true + end + end + + context 'when there is an urgent major release' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'major', urgent: true) + end + + it 'returns false' do + expect(check.pass?).to be false + end + end + + context 'when there is an urgent minor release' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'minor', urgent: true) + end + + it 'returns false' do + expect(check.pass?).to be false + end + end + + context 'when there is an urgent patch release' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true) + end + + it 'returns false' do + expect(check.pass?).to be false + end + end + + context 'when there is a non-urgent patch release' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false) + end + + it 'returns false' do + expect(check.pass?).to be false + end + end + end + + describe 'message' do + context 'when there is a non-urgent patch release pending' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: false) + end + + it 'sends class name symbol to message instance' do + allow(Admin::SystemCheck::Message).to receive(:new) + .with(:software_version_patch_check, anything, anything) + + check.message + + expect(Admin::SystemCheck::Message).to have_received(:new) + .with(:software_version_patch_check, nil, admin_software_updates_path) + end + end + + context 'when there is an urgent patch release pending' do + before do + Fabricate(:software_update, version: '99.99.99', type: 'patch', urgent: true) + end + + it 'sends class name symbol to message instance' do + allow(Admin::SystemCheck::Message).to receive(:new) + .with(:software_version_critical_check, anything, anything, anything) + + check.message + + expect(Admin::SystemCheck::Message).to have_received(:new) + .with(:software_version_critical_check, nil, admin_software_updates_path, true) + end + end + end +end diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 9123804a48..423dce88ab 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -85,4 +85,46 @@ RSpec.describe AdminMailer do expect(mail.body.encoded).to match 'The following items need a review before they can be displayed publicly' end end + + describe '.new_software_updates' do + let(:recipient) { Fabricate(:account, username: 'Bob') } + let(:mail) { described_class.with(recipient: recipient).new_software_updates } + + before do + recipient.user.update(locale: :en) + end + + it 'renders the headers' do + expect(mail.subject).to eq('New Mastodon versions are available for cb6e6126.ngrok.io!') + expect(mail.to).to eq [recipient.user_email] + expect(mail.from).to eq ['notifications@localhost'] + end + + it 'renders the body' do + expect(mail.body.encoded).to match 'New Mastodon versions have been released, you may want to update!' + end + end + + describe '.new_critical_software_updates' do + let(:recipient) { Fabricate(:account, username: 'Bob') } + let(:mail) { described_class.with(recipient: recipient).new_critical_software_updates } + + before do + recipient.user.update(locale: :en) + end + + it 'renders the headers', :aggregate_failures do + expect(mail.subject).to eq('Critical Mastodon updates are available for cb6e6126.ngrok.io!') + expect(mail.to).to eq [recipient.user_email] + expect(mail.from).to eq ['notifications@localhost'] + + expect(mail['Importance'].value).to eq 'high' + expect(mail['Priority'].value).to eq 'urgent' + expect(mail['X-Priority'].value).to eq '1' + end + + it 'renders the body' do + expect(mail.body.encoded).to match 'New critical versions of Mastodon have been released, you may want to update as soon as possible!' + end + end end diff --git a/spec/models/software_update_spec.rb b/spec/models/software_update_spec.rb new file mode 100644 index 0000000000..0a494b0c4c --- /dev/null +++ b/spec/models/software_update_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SoftwareUpdate do + describe '.pending_to_a' do + before do + allow(Mastodon::Version).to receive(:gem_version).and_return(Gem::Version.new(mastodon_version)) + + Fabricate(:software_update, version: '3.4.42', type: 'patch', urgent: true) + Fabricate(:software_update, version: '3.5.0', type: 'minor', urgent: false) + Fabricate(:software_update, version: '4.2.0', type: 'major', urgent: false) + end + + context 'when the Mastodon version is an outdated release' do + let(:mastodon_version) { '3.4.0' } + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('3.4.42', '3.5.0', '4.2.0') + end + end + + context 'when the Mastodon version is more recent than anything last returned by the server' do + let(:mastodon_version) { '5.0.0' } + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to eq [] + end + end + + context 'when the Mastodon version is an outdated nightly' do + let(:mastodon_version) { '4.3.0-nightly.2023-09-10' } + + before do + Fabricate(:software_update, version: '4.3.0-nightly.2023-09-12', type: 'major', urgent: true) + end + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-nightly.2023-09-12') + end + end + + context 'when the Mastodon version is a very outdated nightly' do + let(:mastodon_version) { '4.2.0-nightly.2023-07-10' } + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.2.0') + end + end + + context 'when the Mastodon version is an outdated dev version' do + let(:mastodon_version) { '4.3.0-0.dev.0' } + + before do + Fabricate(:software_update, version: '4.3.0-0.dev.2', type: 'major', urgent: true) + end + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-0.dev.2') + end + end + + context 'when the Mastodon version is an outdated beta version' do + let(:mastodon_version) { '4.3.0-beta1' } + + before do + Fabricate(:software_update, version: '4.3.0-beta2', type: 'major', urgent: true) + end + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-beta2') + end + end + + context 'when the Mastodon version is an outdated beta version and there is a rc' do + let(:mastodon_version) { '4.3.0-beta1' } + + before do + Fabricate(:software_update, version: '4.3.0-rc1', type: 'major', urgent: true) + end + + it 'returns the expected versions' do + expect(described_class.pending_to_a.pluck(:version)).to contain_exactly('4.3.0-rc1') + end + end + end +end diff --git a/spec/policies/software_update_policy_spec.rb b/spec/policies/software_update_policy_spec.rb new file mode 100644 index 0000000000..e19ba61612 --- /dev/null +++ b/spec/policies/software_update_policy_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pundit/rspec' + +RSpec.describe SoftwareUpdatePolicy do + subject { described_class } + + let(:admin) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')).account } + let(:john) { Fabricate(:account) } + + permissions :index? do + context 'when owner' do + it 'permits' do + expect(subject).to permit(admin, SoftwareUpdate) + end + end + + context 'when not owner' do + it 'denies' do + expect(subject).to_not permit(john, SoftwareUpdate) + end + end + end +end diff --git a/spec/services/software_update_check_service_spec.rb b/spec/services/software_update_check_service_spec.rb new file mode 100644 index 0000000000..c8821348ac --- /dev/null +++ b/spec/services/software_update_check_service_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe SoftwareUpdateCheckService, type: :service do + subject { described_class.new } + + shared_examples 'when the feature is enabled' do + let(:full_update_check_url) { "#{update_check_url}?version=#{Mastodon::Version.to_s.split('+')[0]}" } + + let(:devops_role) { Fabricate(:user_role, name: 'DevOps', permissions: UserRole::FLAGS[:view_devops]) } + let(:owner_user) { Fabricate(:user, role: UserRole.find_by(name: 'Owner')) } + let(:old_devops_user) { Fabricate(:user) } + let(:none_user) { Fabricate(:user, role: devops_role) } + let(:patch_user) { Fabricate(:user, role: devops_role) } + let(:critical_user) { Fabricate(:user, role: devops_role) } + + around do |example| + queue_adapter = ActiveJob::Base.queue_adapter + ActiveJob::Base.queue_adapter = :test + + example.run + + ActiveJob::Base.queue_adapter = queue_adapter + end + + before do + Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) + Fabricate(:software_update, version: '42.13.12', type: 'major', urgent: false) + + owner_user.settings.update('notification_emails.software_updates': 'all') + owner_user.save! + + old_devops_user.settings.update('notification_emails.software_updates': 'all') + old_devops_user.save! + + none_user.settings.update('notification_emails.software_updates': 'none') + none_user.save! + + patch_user.settings.update('notification_emails.software_updates': 'patch') + patch_user.save! + + critical_user.settings.update('notification_emails.software_updates': 'critical') + critical_user.save! + end + + context 'when the update server errors out' do + before do + stub_request(:get, full_update_check_url).to_return(status: 404) + end + + it 'deletes outdated update records but keeps valid update records' do + expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['42.13.12']) + end + end + + context 'when the server returns new versions' do + let(:server_json) do + { + updatesAvailable: [ + { + version: '4.2.1', + urgent: false, + type: 'patch', + releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.2.1', + }, + { + version: '4.3.0', + urgent: false, + type: 'minor', + releaseNotes: 'https://github.com/mastodon/mastodon/releases/v4.3.0', + }, + { + version: '5.0.0', + urgent: false, + type: 'minor', + releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', + }, + ], + } + end + + before do + stub_request(:get, full_update_check_url).to_return(body: Oj.dump(server_json)) + end + + it 'updates the list of known updates' do + expect { subject.call }.to change { SoftwareUpdate.pluck(:version).sort }.from(['3.5.0', '42.13.12']).to(['4.2.1', '4.3.0', '5.0.0']) + end + + context 'when no update is urgent' do + it 'sends e-mail notifications according to settings', :aggregate_failures do + expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_software_updates) + .with(hash_including(params: { recipient: owner_user.account })).once + .and(have_enqueued_mail(AdminMailer, :new_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) + .and(have_enqueued_mail.at_most(2)) + end + end + + context 'when an update is urgent' do + let(:server_json) do + { + updatesAvailable: [ + { + version: '5.0.0', + urgent: true, + type: 'minor', + releaseNotes: 'https://github.com/mastodon/mastodon/releases/v5.0.0', + }, + ], + } + end + + it 'sends e-mail notifications according to settings', :aggregate_failures do + expect { subject.call }.to have_enqueued_mail(AdminMailer, :new_critical_software_updates) + .with(hash_including(params: { recipient: owner_user.account })).once + .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: patch_user.account })).once) + .and(have_enqueued_mail(AdminMailer, :new_critical_software_updates).with(hash_including(params: { recipient: critical_user.account })).once) + .and(have_enqueued_mail.at_most(3)) + end + end + end + end + + context 'when update checking is disabled' do + around do |example| + ClimateControl.modify UPDATE_CHECK_URL: '' do + example.run + end + end + + before do + Fabricate(:software_update, version: '3.5.0', type: 'major', urgent: false) + end + + it 'deletes outdated update records' do + expect { subject.call }.to change(SoftwareUpdate, :count).from(1).to(0) + end + end + + context 'when using the default update checking API' do + let(:update_check_url) { 'https://api.joinmastodon.org/update-check' } + + it_behaves_like 'when the feature is enabled' + end + + context 'when using a custom update check URL' do + let(:update_check_url) { 'https://api.example.com/update_check' } + + around do |example| + ClimateControl.modify UPDATE_CHECK_URL: 'https://api.example.com/update_check' do + example.run + end + end + + it_behaves_like 'when the feature is enabled' + end +end diff --git a/spec/workers/scheduler/software_update_check_scheduler_spec.rb b/spec/workers/scheduler/software_update_check_scheduler_spec.rb new file mode 100644 index 0000000000..f596c0a1ec --- /dev/null +++ b/spec/workers/scheduler/software_update_check_scheduler_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Scheduler::SoftwareUpdateCheckScheduler do + subject { described_class.new } + + describe 'perform' do + let(:service_double) { instance_double(SoftwareUpdateCheckService, call: nil) } + + before do + allow(SoftwareUpdateCheckService).to receive(:new).and_return(service_double) + end + + it 'calls SoftwareUpdateCheckService' do + subject.perform + expect(service_double).to have_received(:call) + end + end +end From 05093266e6e3c54f9096da9cdcdafdc83703c578 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 2 Sep 2023 09:02:44 +0200 Subject: [PATCH 6/7] Fix some video encoding failing due to uneven dimensions (#26766) --- app/models/media_attachment.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb index 984f4252a1..f0b072e23f 100644 --- a/app/models/media_attachment.rb +++ b/app/models/media_attachment.rb @@ -102,6 +102,7 @@ class MediaAttachment < ApplicationRecord 'preset' => 'veryfast', 'movflags' => 'faststart', # Move metadata to start of file so playback can begin before download finishes 'pix_fmt' => 'yuv420p', # Ensure color space for cross-browser compatibility + 'vf' => 'crop=floor(iw/2)*2:floor(ih/2)*2', # h264 requires width and height to be even. Crop instead of scale to avoid blurring 'c:v' => 'h264', 'c:a' => 'aac', 'b:a' => '192k', From 728eb6a15387da9074ccc024c9002f74ff829470 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 2 Sep 2023 14:46:26 +0200 Subject: [PATCH 7/7] Fix wrong color on active icons with counters in web UI (#26767) --- .../styles/mastodon/components.scss | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 10083a2a32..ef2b27d6a8 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -228,20 +228,20 @@ color: lighten($lighter-text-color, 7%); background-color: transparent; } + } - &.active { + &.active { + color: $highlight-text-color; + + &:hover, + &:active, + &:focus { color: $highlight-text-color; + background-color: transparent; + } - &:hover, - &:active, - &:focus { - color: $highlight-text-color; - background-color: transparent; - } - - &.disabled { - color: lighten($highlight-text-color, 13%); - } + &.disabled { + color: lighten($highlight-text-color, 13%); } }