1
0
Fork 0
forked from gitea/nas

Merge remote-tracking branch 'parent/main' into kb_migration

This commit is contained in:
KMY 2023-09-03 10:55:46 +09:00
commit 32cfd20257
54 changed files with 1045 additions and 138 deletions

View file

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

View file

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

View file

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

View file

@ -81,7 +81,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':
@ -354,15 +354,19 @@ class Search extends PureComponent {
</>
)}
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
{searchEnabled && (
<>
<h4><FormattedMessage id='search_popout.options' defaultMessage='Search options' /></h4>
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
{label}
</button>
))}
</div>
<div className='search__popout__menu'>
{this.defaultOptions.map(({ key, label, action }, i) => (
<button key={key} onMouseDown={action} className={classNames('search__popout__menu__item', { selected: selectedOption === (options.length + i) })}>
{label}
</button>
))}
</div>
</>
)}
</div>
</div>
);

View file

@ -0,0 +1,26 @@
import { FormattedMessage } from 'react-intl';
export const CriticalUpdateBanner = () => (
<div className='warning-banner'>
<div className='warning-banner__message'>
<h1>
<FormattedMessage
id='home.pending_critical_update.title'
defaultMessage='Critical security update available!'
/>
</h1>
<p>
<FormattedMessage
id='home.pending_critical_update.body'
defaultMessage='Please update your Mastodon server as soon as possible!'
/>{' '}
<a href='/admin/software_updates'>
<FormattedMessage
id='home.pending_critical_update.link'
defaultMessage='See updates'
/>
</a>
</p>
</div>
</div>
);

View file

@ -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(<CriticalUpdateBanner key='critical-update-banner' />);
}
if (tooSlow) {
banner = <ExplorePrompt />;
banners.push(<ExplorePrompt key='explore-prompt' />);
}
return (
@ -196,7 +202,7 @@ class HomeTimeline extends PureComponent {
{signedIn ? (
<StatusListContainer
prepend={banner}
prepend={banners}
alwaysPrepend
trackScroll={!pinned}
scrollKey={`home_timeline-${columnId}`}

View file

@ -91,6 +91,7 @@
* @typedef InitialState
* @property {Record<string, Account>} accounts
* @property {InitialStateLanguage[]} languages
* @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
*/
@ -148,6 +149,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');

View file

@ -318,6 +318,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.",

View file

@ -188,6 +188,7 @@
}
.information-badge,
.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended,
.simple_form .kmyblue {
@ -205,6 +206,7 @@
}
.information-badge,
.simple_form .overridden,
.simple_form .recommended,
.simple_form .not_recommended {
background-color: rgba($ui-secondary-color, 0.1);

View file

@ -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;

View file

@ -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%);
}
}
@ -9219,7 +9219,8 @@ noscript {
}
}
.dismissable-banner {
.dismissable-banner,
.warning-banner {
position: relative;
margin: 10px;
margin-bottom: 5px;
@ -9297,6 +9298,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;

View file

@ -103,6 +103,7 @@ code {
}
}
.overridden,
.recommended,
.not_recommended,
.kmyblue {

View file

@ -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 {

View file

@ -2,6 +2,7 @@
class Admin::SystemCheck
ACTIVE_CHECKS = [
Admin::SystemCheck::SoftwareVersionCheck,
Admin::SystemCheck::MediaPrivacyCheck,
Admin::SystemCheck::DatabaseSchemaCheck,
Admin::SystemCheck::SidekiqProcessCheck,

View file

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

View file

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

View file

@ -3,6 +3,8 @@
class Form::AdminSettings
include ActiveModel::Model
include AuthorizedFetchHelper
KEYS = %i(
site_contact_username
site_contact_email
@ -40,6 +42,7 @@ class Form::AdminSettings
post_hash_tags_max
sensitive_words
sensitive_words_for_full
authorized_fetch
).freeze
INTEGER_KEYS = %i(
@ -63,6 +66,7 @@ class Form::AdminSettings
captcha_enabled
enable_block_emoji_reaction_settings
hide_local_users_for_anonymous
authorized_fetch
).freeze
UPLOAD_KEYS = %i(
@ -70,6 +74,10 @@ class Form::AdminSettings
mascot
).freeze
OVERRIDEN_SETTINGS = {
authorized_fetch: :authorized_fetch_mode?,
}.freeze
STRING_ARRAY_KEYS = %i(
ng_words
sensitive_words
@ -97,6 +105,8 @@ class Form::AdminSettings
SiteUpload.where(var: key).first_or_initialize(var: key)
elsif STRING_ARRAY_KEYS.include?(key)
Setting.public_send(key)&.join("\n") || ''
elsif OVERRIDEN_SETTINGS.include?(key)
public_send(OVERRIDEN_SETTINGS[key])
else
Setting.public_send(key)
end

View file

@ -107,6 +107,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',

View file

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

View file

@ -69,6 +69,7 @@ class UserSettings
setting :trends, default: true
setting :appeal, default: true
setting :warning, default: true
setting :software_updates, default: 'critical', in: %w(none critical patch all)
end
namespace :interactions do

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class SoftwareUpdatePolicy < ApplicationPolicy
def index?
role.can?(:view_devops)
end
end

View file

@ -3,9 +3,13 @@
class InitialStatePresenter < ActiveModelSerializers::Model
attributes :settings, :push_subscription, :token,
:current_account, :admin, :owner, :text, :visibility, :searchability,
: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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 %>

View file

@ -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 %>

View file

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

View file

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

View file

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