Merge branch 'master' into glitch-soc/merge-upstream

Conflicts:
- app/controllers/directories_controller.rb
- package.json
- yarn.lock
This commit is contained in:
Thibaut Girka
2019-07-30 12:22:33 +02:00
59 changed files with 779 additions and 221 deletions

View File

@ -4,6 +4,7 @@ class AboutController < ApplicationController
before_action :set_pack
layout 'public'
before_action :require_open_federation!, only: [:show, :more]
before_action :set_body_classes, only: :show
before_action :set_instance_presenter
before_action :set_expires_in
@ -20,6 +21,10 @@ class AboutController < ApplicationController
private
def require_open_federation!
not_found if whitelist_mode?
end
def new_user
User.new.tap do |user|
user.build_account

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ActivityPub::BaseController < Api::BaseController
skip_before_action :require_authenticated_user!
private
def set_cache_headers

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
class ActivityPub::InboxesController < Api::BaseController
class ActivityPub::InboxesController < ActivityPub::BaseController
include SignatureVerification
include JsonLdHelper
include AccountOwnedConcern

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
class Admin::DomainAllowsController < Admin::BaseController
before_action :set_domain_allow, only: [:destroy]
def new
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(domain: params[:_domain])
end
def create
authorize :domain_allow, :create?
@domain_allow = DomainAllow.new(resource_params)
if @domain_allow.save
log_action :create, @domain_allow
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.created_msg')
else
render :new
end
end
def destroy
authorize @domain_allow, :destroy?
UnallowDomainService.new.call(@domain_allow)
redirect_to admin_instances_path, notice: I18n.t('admin.domain_allows.destroyed_msg')
end
private
def set_domain_allow
@domain_allow = DomainAllow.find(params[:id])
end
def resource_params
params.require(:domain_allow).permit(:domain)
end
end

View File

@ -2,6 +2,10 @@
module Admin
class InstancesController < BaseController
before_action :set_domain_block, only: :show
before_action :set_domain_allow, only: :show
before_action :set_instance, only: :show
def index
authorize :instance, :index?
@ -11,20 +15,38 @@ module Admin
def show
authorize :instance, :show?
@instance = Instance.new(Account.by_domain_accounts.find_by(domain: params[:id]) || DomainBlock.find_by!(domain: params[:id]))
@following_count = Follow.where(account: Account.where(domain: params[:id])).count
@followers_count = Follow.where(target_account: Account.where(domain: params[:id])).count
@reports_count = Report.where(target_account: Account.where(domain: params[:id])).count
@blocks_count = Block.where(target_account: Account.where(domain: params[:id])).count
@available = DeliveryFailureTracker.available?(Account.select(:shared_inbox_url).where(domain: params[:id]).first&.shared_inbox_url)
@media_storage = MediaAttachment.where(account: Account.where(domain: params[:id])).sum(:file_file_size)
@domain_block = DomainBlock.rule_for(params[:id])
end
private
def set_domain_block
@domain_block = DomainBlock.rule_for(params[:id])
end
def set_domain_allow
@domain_allow = DomainAllow.rule_for(params[:id])
end
def set_instance
resource = Account.by_domain_accounts.find_by(domain: params[:id])
resource ||= @domain_block
resource ||= @domain_allow
if resource
@instance = Instance.new(resource)
else
not_found
end
end
def filtered_instances
InstanceFilter.new(filter_params).results
InstanceFilter.new(whitelist_mode? ? { allowed: true } : filter_params).results
end
def paginated_instances

View File

@ -9,6 +9,7 @@ class Api::BaseController < ApplicationController
skip_before_action :store_current_location
skip_before_action :require_functional!
before_action :require_authenticated_user!, if: :disallow_unauthenticated_api_access?
before_action :set_cache_headers
protect_from_forgery with: :null_session
@ -69,6 +70,10 @@ class Api::BaseController < ApplicationController
nil
end
def require_authenticated_user!
render json: { error: 'This API requires an authenticated user' }, status: 401 unless current_user
end
def require_user!
if !current_user
render json: { error: 'This method requires an authenticated user' }, status: 422
@ -94,4 +99,8 @@ class Api::BaseController < ApplicationController
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
def disallow_unauthenticated_api_access?
authorized_fetch_mode?
end
end

View File

@ -12,6 +12,8 @@ class Api::V1::AccountsController < Api::BaseController
before_action :check_account_suspension, only: [:show]
before_action :check_enabled_registrations, only: [:create]
skip_before_action :require_authenticated_user!, only: :create
respond_to :json
def show

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Api::V1::AppsController < Api::BaseController
skip_before_action :require_authenticated_user!
def create
@app = Doorkeeper::Application.create!(application_options)
render json: @app, serializer: REST::ApplicationSerializer

View File

@ -2,6 +2,7 @@
class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json
@ -33,6 +34,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end
def require_enabled_api!
head 404 unless Setting.activity_api_enabled
head 404 unless Setting.activity_api_enabled && !whitelist_mode?
end
end

View File

@ -2,6 +2,7 @@
class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api!
skip_before_action :set_cache_headers
respond_to :json
@ -14,6 +15,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
private
def require_enabled_api!
head 404 unless Setting.peers_api_enabled
head 404 unless Setting.peers_api_enabled && !whitelist_mode?
end
end

View File

@ -2,6 +2,7 @@
class Api::V1::InstancesController < Api::BaseController
respond_to :json
skip_before_action :set_cache_headers
def show

View File

@ -11,6 +11,7 @@ class ApplicationController < ActionController::Base
include UserTrackingConcern
include SessionTrackingConcern
include CacheConcern
include DomainControlHelper
helper_method :current_account
helper_method :current_session
@ -18,6 +19,7 @@ class ApplicationController < ActionController::Base
helper_method :current_skin
helper_method :single_user_mode?
helper_method :use_seamless_external_login?
helper_method :whitelist_mode?
rescue_from ActionController::RoutingError, with: :not_found
rescue_from ActiveRecord::RecordNotFound, with: :not_found
@ -39,7 +41,7 @@ class ApplicationController < ActionController::Base
end
def authorized_fetch_mode?
ENV['AUTHORIZED_FETCH'] == 'true'
ENV['AUTHORIZED_FETCH'] == 'true' || Rails.configuration.x.whitelist_mode
end
def public_fetch_mode?

View File

@ -4,6 +4,7 @@ module AccountOwnedConcern
extend ActiveSupport::Concern
included do
before_action :authenticate_user!, if: -> { whitelist_mode? && request.format != :json }
before_action :set_account, if: :account_required?
before_action :check_account_approval, if: :account_required?
before_action :check_account_suspension, if: :account_required?

View File

@ -3,7 +3,8 @@
class DirectoriesController < ApplicationController
layout 'public'
before_action :check_enabled
before_action :authenticate_user!, if: :whitelist_mode?
before_action :require_enabled!
before_action :set_instance_presenter
before_action :set_tag, only: :show
before_action :set_tags
@ -24,7 +25,7 @@ class DirectoriesController < ApplicationController
use_pack 'share'
end
def check_enabled
def require_enabled!
return not_found unless Setting.profile_directory
end

View File

@ -61,7 +61,7 @@ class HomeController < ApplicationController
end
def default_redirect_path
if request.path.start_with?('/web')
if request.path.start_with?('/web') || whitelist_mode?
new_user_session_path
elsif single_user_mode?
short_account_path(Account.local.without_suspended.where('id > 0').first)

View File

@ -5,6 +5,7 @@ class MediaController < ApplicationController
skip_before_action :store_current_location
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_media_attachment
before_action :verify_permitted_status!
before_action :check_playable, only: :player

View File

@ -5,6 +5,8 @@ class MediaProxyController < ApplicationController
skip_before_action :store_current_location
before_action :authenticate_user!, if: :whitelist_mode?
def show
RedisLock.acquire(lock_options) do |lock|
if lock.acquired?

View File

@ -4,7 +4,8 @@ class PublicTimelinesController < ApplicationController
before_action :set_pack
layout 'public'
before_action :check_enabled
before_action :authenticate_user!, if: :whitelist_mode?
before_action :require_enabled!
before_action :set_body_classes
before_action :set_instance_presenter
@ -17,7 +18,7 @@ class PublicTimelinesController < ApplicationController
private
def check_enabled
def require_enabled!
not_found unless Setting.timeline_preview
end

View File

@ -5,6 +5,7 @@ class RemoteInteractionController < ApplicationController
layout 'modal'
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_interaction_type
before_action :set_status
before_action :set_body_classes

View File

@ -8,6 +8,7 @@ class TagsController < ApplicationController
layout 'public'
before_action :require_signature!, if: -> { request.format == :json && authorized_fetch_mode? }
before_action :authenticate_user!, if: :whitelist_mode?
before_action :set_tag
before_action :set_body_classes
before_action :set_instance_presenter

View File

@ -12,6 +12,14 @@ module DomainControlHelper
end
end
DomainBlock.blocked?(domain)
if whitelist_mode?
!DomainAllow.allowed?(domain)
else
DomainBlock.blocked?(domain)
end
end
def whitelist_mode?
Rails.configuration.x.whitelist_mode
end
end

View File

@ -418,16 +418,16 @@ export function selectComposeSuggestion(position, token, suggestion, path) {
return (dispatch, getState) => {
let completion, startPosition;
if (typeof suggestion === 'object' && suggestion.id) {
if (suggestion.type === 'emoji') {
completion = suggestion.native || suggestion.colons;
startPosition = position - 1;
dispatch(useEmoji(suggestion));
} else if (typeof suggestion === 'object' && suggestion.name) {
} else if (suggestion.type === 'hashtag') {
completion = `#${suggestion.name}`;
startPosition = position - 1;
} else {
completion = getState().getIn(['accounts', suggestion, 'acct']);
} else if (suggestion.type === 'account') {
completion = getState().getIn(['accounts', suggestion.id, 'acct']);
startPosition = position;
}

View File

@ -168,15 +168,15 @@ export default class AutosuggestInput extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object' && suggestion.shortcode) {
if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (typeof suggestion === 'object' && suggestion.name) {
} else if (suggestion.type ==='hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (

View File

@ -174,15 +174,15 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
const { selectedSuggestion } = this.state;
let inner, key;
if (typeof suggestion === 'object' && suggestion.shortcode) {
if (suggestion.type === 'emoji') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else if (typeof suggestion === 'object' && suggestion.name) {
} else if (suggestion.type === 'hashtag') {
inner = <AutosuggestHashtag tag={suggestion} />;
key = suggestion.name;
} else {
inner = <AutosuggestAccountContainer id={suggestion} />;
key = suggestion;
} else if (suggestion.type === 'account') {
inner = <AutosuggestAccountContainer id={suggestion.id} />;
key = suggestion.id;
}
return (

View File

@ -8,9 +8,71 @@ import classnames from 'classnames';
import PollContainer from 'mastodon/containers/poll_container';
import Icon from 'mastodon/components/icon';
import { autoPlayGif } from 'mastodon/initial_state';
import { decode as decodeIDNA } from 'mastodon/utils/idna';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
// Regex matching what "looks like a link", that is, something that starts with
// an optional "http://" or "https://" scheme and then what could look like a
// domain main, that is, at least two sequences of characters not including spaces
// and separated by "." or an homoglyph. The idea is not to match valid URLs or
// domain names, but what could be confused for a valid URL or domain name,
// especially to the untrained eye.
const h_confusables = 'h\u13c2\u1d58d\u1d4f1\u1d691\u0068\uff48\u1d525\u210e\u1d489\u1d629\u0570\u1d4bd\u1d65d\u1d421\u1d5c1\u1d5f5\u04bb\u1d559';
const t_confusables = 't\u1d42d\u1d5cd\u1d531\u1d565\u1d4c9\u1d669\u1d4fd\u1d69d\u0074\u1d461\u1d601\u1d495\u1d635\u1d599';
const p_confusables = 'p\u0440\u03c1\u1d52d\u1d631\u1d665\u1d429\uff50\u1d6e0\u1d45d\u1d561\u1d595\u1d71a\u1d699\u1d78e\u2ca3\u1d754\u1d6d2\u1d491\u1d7c8\u1d746\u1d4c5\u1d70c\u1d5c9\u0070\u1d780\u03f1\u1d5fd\u2374\u1d7ba\u1d4f9';
const s_confusables = 's\u1d530\u118c1\u1d494\u1d634\u1d4c8\u1d668\uabaa\u1d42c\u1d5cc\u1d460\u1d600\ua731\u0073\uff53\u1d564\u0455\u1d598\u1d4fc\u1d69c\u10448\u01bd';
const column_confusables = ':\u0903\u0a83\u0703\u1803\u05c3\u0704\u0589\u1809\ua789\u16ec\ufe30\u02d0\u2236\u02f8\u003a\uff1a\u205a\ua4fd';
const slash_confusables = '/\u2041\u2f03\u2044\u2cc6\u27cb\u30ce\u002f\u2571\u31d3\u3033\u1735\u2215\u29f8\u1d23a\u4e3f';
const dot_confusables = '.\u002e\u0660\u06f0\u0701\u0702\u2024\ua4f8\ua60e\u10a50\u1d16d';
const linkRegex = new RegExp(`^\\s*(([${h_confusables}][${t_confusables}][${t_confusables}][${p_confusables}][${s_confusables}]?[${column_confusables}][${slash_confusables}][${slash_confusables}]))?[^:/\\n ]+([${dot_confusables}][^:/\\n ]+)+`);
const isLinkMisleading = (link) => {
let linkTextParts = [];
// Reconstruct visible text, as we do not have much control over how links
// from remote software look, and we can't rely on `innerText` because the
// `invisible` class does not set `display` to `none`.
const walk = (node) => {
switch (node.nodeType) {
case Node.TEXT_NODE:
linkTextParts.push(node.textContent);
break;
case Node.ELEMENT_NODE:
if (node.classList.contains('invisible')) return;
const children = node.childNodes;
for (let i = 0; i < children.length; i++) {
walk(children[i]);
}
break;
}
};
walk(link);
const linkText = linkTextParts.join('');
const targetURL = new URL(link.href);
// The following may not work with international domain names
if (linkText === targetURL.origin || linkText === targetURL.host || 'www.' + linkText === targetURL.host || linkText.startsWith(targetURL.origin + '/') || linkText.startsWith(targetURL.host + '/')) {
return false;
}
// The link hasn't been recognized, maybe it features an international domain name
const hostname = decodeIDNA(targetURL.hostname);
const host = targetURL.host.replace(targetURL.hostname, hostname);
const origin = targetURL.origin.replace(targetURL.host, host);
if (linkText === origin || linkText === host || linkText.startsWith(origin + '/') || linkText.startsWith(host + '/')) {
return false;
}
// If the link text looks like an URL or auto-generated link, it is misleading
return linkRegex.test(linkText);
};
export default class StatusContent extends React.PureComponent {
static contextTypes = {
@ -56,6 +118,34 @@ export default class StatusContent extends React.PureComponent {
} else {
link.setAttribute('title', link.href);
link.classList.add('unhandled-link');
if (isLinkMisleading(link)) {
while (link.firstChild) {
link.removeChild(link.firstChild);
}
const prefix = (link.href.match(/https?:\/\/(www\.)?/) || [''])[0];
const text = link.href.substr(prefix.length, 30);
const suffix = link.href.substr(prefix.length + 30);
const cutoff = !!suffix;
const prefixTag = document.createElement('span');
prefixTag.classList.add('invisible');
prefixTag.textContent = prefix;
link.appendChild(prefixTag);
const textTag = document.createElement('span');
if (cutoff) {
textTag.classList.add('ellipsis');
}
textTag.textContent = text;
link.appendChild(textTag);
const suffixTag = document.createElement('span');
suffixTag.classList.add('invisible');
suffixTag.textContent = suffix;
link.appendChild(suffixTag);
}
}
link.setAttribute('target', '_blank');

View File

@ -2,18 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import punycode from 'punycode';
import classnames from 'classnames';
import Icon from 'mastodon/components/icon';
const IDNA_PREFIX = 'xn--';
const decodeIDNA = domain => {
return domain
.split('.')
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
.join('.');
};
import { decode as decodeIDNA } from 'mastodon/utils/idna';
const getHostname = url => {
const parser = document.createElement('a');

View File

@ -207,11 +207,11 @@ const expiresInFromExpiresAt = expires_at => {
const normalizeSuggestions = (state, { accounts, emojis, tags }) => {
if (accounts) {
return accounts.map(item => item.id);
return accounts.map(item => ({ id: item.id, type: 'account' }));
} else if (emojis) {
return emojis;
return emojis.map(item => ({ ...item, type: 'emoji' }));
} else {
return sortHashtagsByUse(state, tags);
return sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' })));
}
};

View File

@ -44,7 +44,8 @@ export default function search(state = initialState, action) {
hashtags: fromJS(action.results.hashtags),
})).set('submitted', true).set('searchTerm', action.searchTerm);
case SEARCH_EXPAND_SUCCESS:
return state.updateIn(['results', action.searchType], list => list.concat(action.results[action.searchType].map(item => item.id)));
const results = action.searchType === 'hashtags' ? fromJS(action.results.hashtags) : action.results[action.searchType].map(item => item.id);
return state.updateIn(['results', action.searchType], list => list.concat(results));
default:
return state;
}

View File

@ -0,0 +1,10 @@
import punycode from 'punycode';
const IDNA_PREFIX = 'xn--';
export const decode = domain => {
return domain
.split('.')
.map(part => part.indexOf(IDNA_PREFIX) === 0 ? punycode.decode(part.slice(IDNA_PREFIX.length)) : part)
.join('.');
};

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: domain_allows
#
# id :bigint(8) not null, primary key
# domain :string default(""), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class DomainAllow < ApplicationRecord
include DomainNormalizable
validates :domain, presence: true, uniqueness: true
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
class << self
def allowed?(domain)
!rule_for(domain).nil?
end
def rule_for(domain)
return if domain.blank?
uri = Addressable::URI.new.tap { |u| u.host = domain.gsub(/[\/]/, '') }
find_by(domain: uri.normalized_host)
end
end
end

View File

@ -7,8 +7,9 @@ class Instance
def initialize(resource)
@domain = resource.domain
@accounts_count = resource.is_a?(DomainBlock) ? nil : resource.accounts_count
@accounts_count = resource.respond_to?(:accounts_count) ? resource.accounts_count : nil
@domain_block = resource.is_a?(DomainBlock) ? resource : DomainBlock.rule_for(domain)
@domain_allow = resource.is_a?(DomainAllow) ? resource : DomainAllow.rule_for(domain)
end
def countable?

View File

@ -12,6 +12,10 @@ class InstanceFilter
scope = DomainBlock
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
scope.order(id: :desc)
elsif params[:allowed].present?
scope = DomainAllow
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?
scope.order(id: :desc)
else
scope = Account.remote
scope = scope.matches_domain(params[:by_domain]) if params[:by_domain].present?

View File

@ -65,7 +65,7 @@ class Tag < ApplicationRecord
class << self
def find_or_create_by_names(name_or_names)
Array(name_or_names).map(&method(:normalize)).uniq.map do |normalized_name|
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
tag = matching_name(normalized_name).first || create(name: normalized_name)
yield tag if block_given?
@ -77,7 +77,7 @@ class Tag < ApplicationRecord
def search_for(term, limit = 5, offset = 0)
pattern = sanitize_sql_like(normalize(term.strip)) + '%'
Tag.where(arel_table[:name].lower.matches(pattern.downcase))
Tag.where(arel_table[:name].lower.matches(pattern.mb_chars.downcase.to_s))
.order(:name)
.limit(limit)
.offset(offset)
@ -92,7 +92,7 @@ class Tag < ApplicationRecord
end
def matching_name(name_or_names)
names = Array(name_or_names).map { |name| normalize(name).downcase }
names = Array(name_or_names).map { |name| normalize(name).mb_chars.downcase.to_s }
if names.size == 1
where(arel_table[:name].lower.eq(names.first))
@ -104,7 +104,7 @@ class Tag < ApplicationRecord
private
def normalize(str)
str.gsub(/\A#/, '').mb_chars.to_s
str.gsub(/\A#/, '')
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class DomainAllowPolicy < ApplicationPolicy
def create?
admin?
end
def destroy?
admin?
end
end

View File

@ -14,6 +14,6 @@ module Payloadable
end
def signing_enabled?
ENV['AUTHORIZED_FETCH'] != 'true'
ENV['AUTHORIZED_FETCH'] != 'true' && !Rails.configuration.x.whitelist_mode
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class UnallowDomainService < BaseService
def call(domain_allow)
Account.where(domain: domain_allow.domain).find_each do |account|
SuspendAccountService.new.call(account, destroy: true)
end
domain_allow.destroy
end
end

View File

@ -0,0 +1,14 @@
- content_for :header_tags do
= javascript_pack_tag 'admin', integrity: true, async: true, crossorigin: 'anonymous'
- content_for :page_title do
= t('admin.domain_allows.add_new')
= simple_form_for @domain_allow, url: admin_domain_allows_path do |f|
= render 'shared/error_messages', object: @domain_allow
.fields-group
= f.input :domain, wrapper: :with_label, label: t('admin.domain_blocks.domain'), required: true
.actions
= f.button :button, t('admin.domain_allows.add_new'), type: :submit

View File

@ -6,24 +6,30 @@
%strong= t('admin.instances.moderation.title')
%ul
%li= filter_link_to t('admin.instances.moderation.all'), limited: nil
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
- unless whitelist_mode?
%li= filter_link_to t('admin.instances.moderation.limited'), limited: '1'
%div{ style: 'flex: 1 1 auto; text-align: right' }
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
- if whitelist_mode?
= link_to t('admin.domain_allows.add_new'), new_admin_domain_allow_path, class: 'button'
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path, class: 'button'
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::INSTANCES_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
- unless whitelist_mode?
= form_tag admin_instances_url, method: 'GET', class: 'simple_form' do
.fields-group
- Admin::FilterHelper::INSTANCES_FILTERS.each do |key|
- if params[key].present?
= hidden_field_tag key, params[key]
- %i(by_domain).each do |key|
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}")
- %i(by_domain).each do |key|
.input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.instances.#{key}")
.actions
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative'
.actions
%button= t('admin.accounts.search')
= link_to t('admin.accounts.reset'), admin_instances_path, class: 'button negative'
%hr.spacer/
@ -47,8 +53,11 @@
- unless first_item
&bull;
= t('admin.domain_blocks.rejecting_reports')
- elsif whitelist_mode?
= t('admin.accounts.whitelisted')
- else
= t('admin.accounts.no_limits_imposed')
- if instance.countable?
.trends__item__current{ title: t('admin.instances.known_accounts', count: instance.accounts_count) }= number_to_human instance.accounts_count, strip_insignificant_zeros: true
= paginate paginated_instances

View File

@ -38,7 +38,9 @@
= link_to t('admin.accounts.title'), admin_accounts_path(remote: '1', by_domain: @instance.domain), class: 'button'
%div{ style: 'float: right' }
- if @domain_block
- if @domain_allow
= link_to t('admin.domain_allows.undo'), admin_domain_allow_path(@domain_allow), class: 'button button--destructive', data: { confirm: t('admin.accounts.are_you_sure'), method: :delete }
- elsif @domain_block
= link_to t('admin.domain_blocks.undo'), admin_domain_block_path(@domain_block), class: 'button'
- else
= link_to t('admin.domain_blocks.add_new'), new_admin_domain_block_path(_domain: @instance.domain), class: 'button'

View File

@ -42,11 +42,12 @@
%hr.spacer/
.fields-group
= f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
- unless whitelist_mode?
.fields-group
= f.input :timeline_preview, as: :boolean, wrapper: :with_label, label: t('admin.settings.timeline_preview.title'), hint: t('admin.settings.timeline_preview.desc_html')
.fields-group
= f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
.fields-group
= f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
.fields-group
= f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
@ -54,17 +55,18 @@
.fields-group
= f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
.fields-group
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html')
- unless whitelist_mode?
.fields-group
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html')
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html')
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html')
.fields-group
= f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
.fields-group
= f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
.fields-group
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
.fields-group
= f.input :hide_followers_count, as: :boolean, wrapper: :with_label, label: t('admin.settings.hide_followers_count.title'), hint: t('admin.settings.hide_followers_count.desc_html')
@ -88,7 +90,7 @@
.fields-group
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 }
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')

View File

@ -33,7 +33,7 @@
= f.input :invite_code, as: :hidden
.fields-group
= f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path)
= f.input :agreement, as: :boolean, wrapper: :with_label, label: whitelist_mode? ? t('auth.checkbox_agreement_without_rules_html', terms_path: terms_path) : t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path)
.actions
= f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit

View File

@ -7,10 +7,13 @@
= link_to root_url, class: 'brand' do
= svg_logo_full
= link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory
= link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
- unless whitelist_mode?
= link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory
= link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
.nav-center
.nav-right
- if user_signed_in?
= link_to t('settings.back'), root_url, class: 'nav-link nav-button webapp-btn'