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

Conflicts:
- `app/controllers/application_controller.rb`:
  Conflict due to theming system.
- `app/controllers/oauth/authorizations_controller.rb`:
  Conflict due to theming system.
signup-info-prompt
Thibaut Girka 2020-01-04 22:54:06 +01:00
commit 01eaeab56d
72 changed files with 708 additions and 376 deletions

View File

@ -3,7 +3,7 @@ version: 2
aliases:
- &defaults
docker:
- image: circleci/ruby:2.6-stretch-node
- image: circleci/ruby:2.6-buster-node
environment: &ruby_environment
BUNDLE_APP_CONFIG: ./.bundle/
DB_HOST: localhost
@ -39,7 +39,6 @@ aliases:
steps:
- checkout
- *attach_workspace
- restore_cache:
keys:
- v1-node-dependencies-{{ checksum "yarn.lock" }}
@ -49,7 +48,6 @@ aliases:
key: v1-node-dependencies-{{ checksum "yarn.lock" }}
paths:
- ./node_modules/
- *persist_to_workspace
- &install_system_dependencies
@ -59,12 +57,16 @@ aliases:
sudo apt-get update
sudo apt-get install -y libicu-dev libidn11-dev libprotobuf-dev protobuf-compiler
## TODO: FIX THESE BUSTER DEPENDANCES
sudo wget http://ftp.au.debian.org/debian/pool/main/i/icu/libicu57_57.1-6+deb9u3_amd64.deb
sudo dpkg -i libicu57_57.1-6+deb9u3_amd64.deb
sudo wget http://ftp.au.debian.org/debian/pool/main/p/protobuf/libprotobuf10_3.0.0-9_amd64.deb
sudo dpkg -i libprotobuf10_3.0.0-9_amd64.deb
- &install_ruby_dependencies
steps:
- *attach_workspace
- *install_system_dependencies
- run: ruby -e 'puts RUBY_VERSION' | tee /tmp/.ruby-version
- *restore_ruby_dependencies
- run: bundle install --clean --jobs 16 --path ./vendor/bundle/ --retry 3 --with pam_authentication --without development production && bundle clean
@ -82,10 +84,8 @@ aliases:
- &test_steps
steps:
- *attach_workspace
- *install_system_dependencies
- run: sudo apt-get install -y ffmpeg
- run:
name: Prepare Tests
command: ./bin/rails parallel:create parallel:load_schema parallel:prepare
@ -105,14 +105,14 @@ jobs:
install-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5-stretch-node
- image: circleci/ruby:2.5-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
install-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4-stretch-node
- image: circleci/ruby:2.4-buster-node
environment: *ruby_environment
<<: *install_ruby_dependencies
@ -131,7 +131,7 @@ jobs:
test-ruby2.6:
<<: *defaults
docker:
- image: circleci/ruby:2.6-stretch-node
- image: circleci/ruby:2.6-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
@ -142,7 +142,7 @@ jobs:
test-ruby2.5:
<<: *defaults
docker:
- image: circleci/ruby:2.5-stretch-node
- image: circleci/ruby:2.5-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
@ -153,7 +153,7 @@ jobs:
test-ruby2.4:
<<: *defaults
docker:
- image: circleci/ruby:2.4-stretch-node
- image: circleci/ruby:2.4-buster-node
environment: *ruby_environment
- image: circleci/postgres:10.6-alpine
environment:
@ -164,7 +164,7 @@ jobs:
test-webui:
<<: *defaults
docker:
- image: circleci/node:12.9-stretch
- image: circleci/node:12-buster
steps:
- *attach_workspace
- run: ./bin/retry yarn test:jest

View File

@ -4,7 +4,7 @@ FROM ubuntu:18.04 as build-dep
SHELL ["bash", "-c"]
# Install Node v12 (LTS)
ENV NODE_VER="12.13.1"
ENV NODE_VER="12.14.0"
RUN echo "Etc/UTC" > /etc/localtime && \
apt update && \
apt -y install wget python && \

14
Gemfile
View File

@ -7,11 +7,11 @@ gem 'pkg-config', '~> 1.4'
gem 'puma', '~> 4.3'
gem 'rails', '~> 5.2.4'
gem 'sprockets', '~> 3.7'
gem 'sprockets', '~> 3.7.2'
gem 'thor', '~> 0.20'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 1.1'
gem 'pg', '~> 1.2'
gem 'makara', '~> 0.4'
gem 'pghero', '~> 2.4'
gem 'dotenv-rails', '~> 2.7'
@ -31,7 +31,7 @@ gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'iso-639'
gem 'chewy', '~> 5.1'
gem 'cld3', '~> 3.2.4'
gem 'cld3', '~> 3.2.6'
gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
@ -50,7 +50,7 @@ gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'goldfinger', '~> 2.1'
gem 'hiredis', '~> 0.6'
gem 'redis-namespace', '~> 1.5'
gem 'redis-namespace', '~> 1.7'
gem 'health_check', git: 'https://github.com/ianheggie/health_check', ref: '0b799ead604f900ed50685e9b2d469cd2befba5b'
gem 'html2text'
gem 'htmlentities', '~> 4.3'
@ -61,7 +61,7 @@ gem 'httplog', '~> 1.3'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.3', require: 'mime/types/columnar'
gem 'mime-types', '~> 3.3.1', require: 'mime/types/columnar'
gem 'nilsimsa', git: 'https://github.com/witgo/nilsimsa', ref: 'fd184883048b922b176939f851338d0a4971a532'
gem 'nokogiri', '~> 1.10'
gem 'nsa', '~> 0.2'
@ -120,8 +120,8 @@ end
group :test do
gem 'capybara', '~> 3.29'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 2.9'
gem 'microformats', '~> 4.1'
gem 'faker', '~> 2.10'
gem 'microformats', '~> 4.2'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.17', require: false

View File

@ -185,10 +185,10 @@ GEM
crack (0.4.3)
safe_yaml (~> 1.0.0)
crass (1.0.5)
css_parser (1.7.0)
css_parser (1.7.1)
addressable
debug_inspector (0.0.3)
derailed_benchmarks (1.4.2)
derailed_benchmarks (1.4.3)
benchmark-ips (~> 2)
get_process_mem (~> 0)
heapy (~> 0)
@ -240,9 +240,9 @@ GEM
tzinfo
excon (0.71.0)
fabrication (2.21.0)
faker (2.9.0)
faker (2.10.0)
i18n (>= 1.6, < 1.8)
faraday (0.15.4)
faraday (1.0.0)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fastimage (2.1.7)
@ -275,8 +275,8 @@ GEM
http (~> 3.0)
nokogiri (~> 1.8)
oj (~> 3.0)
hamlit (2.9.3)
temple (>= 0.8.0)
hamlit (2.11.0)
temple (>= 0.8.2)
thor
tilt
hamlit-rails (0.2.3)
@ -374,9 +374,9 @@ GEM
microformats (4.1.0)
json (~> 2.1)
nokogiri (~> 1.8, >= 1.8.3)
mime-types (3.3)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0904)
mime-types-data (3.2019.1009)
mimemagic (0.3.3)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
@ -434,7 +434,7 @@ GEM
pastel (0.7.3)
equatable (~> 0.6)
tty-color (~> 0.5)
pg (1.1.4)
pg (1.2.0)
pghero (2.4.1)
activerecord (>= 5)
pkg-config (1.4.0)
@ -454,7 +454,7 @@ GEM
pry (~> 0.10)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.1)
public_suffix (4.0.2)
puma (4.3.1)
nio4r (~> 2.0)
pundit (2.1.0)
@ -463,7 +463,7 @@ GEM
rack (2.0.8)
rack-attack (6.2.2)
rack (>= 1.0, < 3)
rack-cors (1.1.0)
rack-cors (1.1.1)
rack (>= 2.0.0)
rack-protection (2.0.7)
rack
@ -520,7 +520,7 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
redis-namespace (1.6.0)
redis-namespace (1.7.0)
redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
@ -532,7 +532,7 @@ GEM
redis-store (1.5.0)
redis (>= 2.2, < 5)
regexp_parser (1.6.0)
request_store (1.4.1)
request_store (1.5.0)
rack (>= 1.4)
responders (3.0.0)
actionpack (>= 5.0)
@ -618,21 +618,21 @@ GEM
sshkit (1.20.0)
net-scp (>= 1.1.2)
net-ssh (>= 2.8.0)
stackprof (0.2.14)
stackprof (0.2.15)
statsd-ruby (1.4.0)
stoplight (2.2.0)
streamio-ffmpeg (3.0.2)
multi_json (~> 1.8)
strong_migrations (0.5.1)
activerecord (>= 5)
temple (0.8.1)
temple (0.8.2)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
terrapin (0.6.0)
climate_control (>= 0.0.3, < 1.0)
thor (0.20.3)
thread_safe (0.3.6)
tilt (2.0.9)
tilt (2.0.10)
tty-color (0.5.0)
tty-command (0.9.0)
pastel (~> 0.7.0)
@ -654,7 +654,7 @@ GEM
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unf_ext (0.0.7.6)
unicode-display_width (1.6.0)
uniform_notifier (1.12.1)
warden (1.2.8)
@ -713,7 +713,7 @@ DEPENDENCIES
doorkeeper (~> 5.2)
dotenv-rails (~> 2.7)
fabrication (~> 2.21)
faker (~> 2.9)
faker (~> 2.10)
fast_blank (~> 1.0)
fastimage
fog-core (<= 2.1.0)
@ -759,7 +759,7 @@ DEPENDENCIES
parallel (~> 1.19)
parallel_tests (~> 2.30)
parslet
pg (~> 1.1)
pg (~> 1.2)
pghero (~> 2.4)
pkg-config (~> 1.4)
posix-spawn!
@ -778,7 +778,7 @@ DEPENDENCIES
rdf-normalize (~> 0.3)
redcarpet (~> 3.4)
redis (~> 4.1)
redis-namespace (~> 1.5)
redis-namespace (~> 1.7)
redis-rails (~> 5.0)
rqrcode (~> 0.10)
rspec-rails (~> 3.9)
@ -808,9 +808,3 @@ DEPENDENCIES
webmock (~> 3.7)
webpacker (~> 4.2)
webpush
RUBY VERSION
ruby 2.6.5p114
BUNDLED WITH
1.17.3

View File

@ -2,10 +2,6 @@
module Admin
class CustomEmojisController < BaseController
include ObfuscateFilename
obfuscate_filename [:custom_emoji, :image]
def index
authorize :custom_emoji, :index?

View File

@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowerAccountsController < Api::BaseController
def load_accounts
return [] if hide_results?
default_accounts.merge(paginated_follows).to_a
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_follows).to_a
end
def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View File

@ -21,11 +21,13 @@ class Api::V1::Accounts::FollowingAccountsController < Api::BaseController
def load_accounts
return [] if hide_results?
default_accounts.merge(paginated_follows).to_a
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_follows).to_a
end
def hide_results?
(@account.user_hides_network? && current_account.id != @account.id) || (current_account && @account.blocking?(current_account))
(@account.user_hides_network? && current_account&.id != @account.id) || (current_account && @account.blocking?(current_account))
end
def default_accounts

View File

@ -4,9 +4,6 @@ class Api::V1::MediaController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:media' }
before_action :require_user!
include ObfuscateFilename
obfuscate_filename :file
respond_to :json
def create

View File

@ -17,7 +17,9 @@ class Api::V1::Statuses::FavouritedByAccountsController < Api::BaseController
private
def load_accounts
default_accounts.merge(paginated_favourites).to_a
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_favourites).to_a
end
def default_accounts

View File

@ -17,7 +17,9 @@ class Api::V1::Statuses::RebloggedByAccountsController < Api::BaseController
private
def load_accounts
default_accounts.merge(paginated_statuses).to_a
scope = default_accounts
scope = scope.where.not(id: current_account.excluded_from_timeline_account_ids) unless current_account.nil?
scope.merge(paginated_statuses).to_a
end
def default_accounts

View File

@ -25,6 +25,7 @@ class ApplicationController < ActionController::Base
rescue_from ActionController::InvalidAuthenticityToken, with: :unprocessable_entity
rescue_from ActionController::UnknownFormat, with: :not_acceptable
rescue_from ActionController::ParameterMissing, with: :bad_request
rescue_from Paperclip::AdapterRegistry::NoHandlerError, with: :bad_request
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Mastodon::NotPermittedError, with: :forbidden
rescue_from HTTP::Error, OpenSSL::SSL::SSLError, with: :internal_server_error
@ -211,7 +212,12 @@ class ApplicationController < ActionController::Base
end
def respond_with_error(code)
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
respond_to do |format|
format.any do
use_pack 'error'
render "errors/#{code}", layout: 'error', status: code, formats: [:html]
end
format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
end
end
end

View File

@ -11,6 +11,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
before_action :set_instance_presenter, only: [:new, :create, :update]
before_action :set_body_classes, only: [:new, :create, :edit, :update]
before_action :require_not_suspended!, only: [:update]
before_action :set_cache_headers, only: [:edit, :update]
skip_before_action :require_functional!, only: [:edit, :update]
@ -114,4 +115,8 @@ class Auth::RegistrationsController < Devise::RegistrationsController
def require_not_suspended!
forbidden if current_account.suspended?
end
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
module ObfuscateFilename
extend ActiveSupport::Concern
class_methods do
def obfuscate_filename(path)
before_action do
file = params.dig(*path)
next if file.nil?
file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
end
end
end
end

View File

@ -1,10 +1,9 @@
# frozen_string_literal: true
class FiltersController < ApplicationController
include Authorization
layout 'admin'
before_action :authenticate_user!
before_action :set_filters, only: :index
before_action :set_filter, only: [:edit, :update, :destroy]
before_action :set_pack

View File

@ -19,7 +19,6 @@ class FollowerAccountsController < ApplicationController
next if @account.user_hides_network?
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:account_id), current_user.account_id) if user_signed_in?
end
format.json do
@ -38,7 +37,11 @@ class FollowerAccountsController < ApplicationController
private
def follows
@follows ||= Follow.where(target_account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
return @follows if defined?(@follows)
scope = Follow.where(target_account: @account)
scope = scope.where.not(account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:account)
end
def page_requested?

View File

@ -19,7 +19,6 @@ class FollowingAccountsController < ApplicationController
next if @account.user_hides_network?
follows
@relationships = AccountRelationshipsPresenter.new(follows.map(&:target_account_id), current_user.account_id) if user_signed_in?
end
format.json do
@ -38,7 +37,11 @@ class FollowingAccountsController < ApplicationController
private
def follows
@follows ||= Follow.where(account: @account).recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
return @follows if defined?(@follows)
scope = Follow.where(account: @account)
scope = scope.where.not(target_account_id: current_account.excluded_from_timeline_account_ids) if user_signed_in?
@follows = scope.recent.page(params[:page]).per(FOLLOW_PER_PAGE).preload(:target_account)
end
def page_requested?

View File

@ -6,6 +6,7 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
before_action :store_current_location
before_action :authenticate_resource_owner!
before_action :set_pack
before_action :set_cache_headers
include Localized
@ -32,4 +33,8 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
def truthy_param?(key)
ActiveModel::Type::Boolean.new.cast(params[key])
end
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
end

View File

@ -3,6 +3,7 @@
class Settings::BaseController < ApplicationController
before_action :set_pack
before_action :set_body_classes
before_action :set_cache_headers
private
@ -13,4 +14,8 @@ class Settings::BaseController < ApplicationController
def set_body_classes
@body_classes = 'admin'
end
def set_cache_headers
response.headers['Cache-Control'] = 'no-cache, no-store, max-age=0, must-revalidate'
end
end

View File

@ -1,16 +1,11 @@
# frozen_string_literal: true
class Settings::ProfilesController < Settings::BaseController
include ObfuscateFilename
layout 'admin'
before_action :authenticate_user!
before_action :set_account
obfuscate_filename [:account, :avatar]
obfuscate_filename [:account, :header]
def show
@account.build_fields
end

View File

@ -8,12 +8,8 @@ module WellKnown
def show
@webfinger_template = "#{webfinger_url}?resource={uri}"
respond_to do |format|
format.xml { render content_type: 'application/xrd+xml' }
end
expires_in 3.days, public: true
render content_type: 'application/xrd+xml', formats: [:xml]
end
end
end

View File

@ -13,7 +13,7 @@ module AccountsHelper
if account.local?
"@#{account.acct}@#{Rails.configuration.x.local_domain}"
else
"@#{account.acct}"
"@#{account.pretty_acct}"
end
end

View File

@ -26,8 +26,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_REVEAL = 'STATUS_REVEAL';
export const STATUS_HIDE = 'STATUS_HIDE';
export const STATUS_COLLAPSE = 'STATUS_COLLAPSE';
export const REDRAFT = 'REDRAFT';
@ -320,3 +321,11 @@ export function revealStatus(ids) {
ids,
};
};
export function toggleStatusCollapse(id, isCollapsed) {
return {
type: STATUS_COLLAPSE,
id,
isCollapsed,
};
}

View File

@ -208,10 +208,13 @@ export default class ScrollableList extends PureComponent {
}
attachIntersectionObserver () {
this.intersectionObserverWrapper.connect({
let nodeOptions = {
root: this.node,
rootMargin: '300% 0px',
});
};
this.intersectionObserverWrapper
.connect(this.props.bindToDocument ? {} : nodeOptions);
}
detachIntersectionObserver () {

View File

@ -76,6 +76,7 @@ class Status extends ImmutablePureComponent {
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
onToggleCollapsed: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
@ -102,19 +103,6 @@ class Status extends ImmutablePureComponent {
statusId: undefined,
};
// Track height changes we know about to compensate scrolling
componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
getSnapshotBeforeUpdate () {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
return null;
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
@ -126,32 +114,6 @@ class Status extends ImmutablePureComponent {
}
}
// Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
@ -196,7 +158,11 @@ class Status extends ImmutablePureComponent {
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
}
handleCollapsedToggle = isCollapsed => {
this.props.onToggleCollapsed(this._properStatus(), isCollapsed);
}
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
@ -466,7 +432,7 @@ class Status extends ImmutablePureComponent {
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
{media}

View File

@ -23,11 +23,11 @@ export default class StatusContent extends React.PureComponent {
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
onCollapsedToggle: PropTypes.func,
};
state = {
hidden: true,
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
};
_updateStatusLinks () {
@ -62,14 +62,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('rel', 'noopener noreferrer');
}
if (
this.props.collapsable
&& this.props.onClick
&& this.state.collapsed === null
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0
) {
this.setState({ collapsed: true });
if (this.props.status.get('collapsed', null) === null) {
let collapsed =
this.props.collapsable
&& this.props.onClick
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0;
if(this.props.onCollapsedToggle) this.props.onCollapsedToggle(collapsed);
this.props.status.set('collapsed', collapsed);
}
}
@ -178,6 +180,7 @@ export default class StatusContent extends React.PureComponent {
}
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed');
const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') };
@ -185,7 +188,7 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': this.state.collapsed === true,
'status__content--collapsed': renderReadMore,
});
if (isRtl(status.get('search_index'))) {
@ -237,7 +240,7 @@ export default class StatusContent extends React.PureComponent {
</div>,
];
if (this.state.collapsed) {
if (renderReadMore) {
output.push(readMoreButton);
}

View File

@ -23,6 +23,7 @@ import {
deleteStatus,
hideStatus,
revealStatus,
toggleStatusCollapse,
} from '../actions/statuses';
import {
unmuteAccount,
@ -190,6 +191,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
}
},
onToggleCollapsed (status, isCollapsed) {
dispatch(toggleStatusCollapse(status.get('id'), isCollapsed));
},
onBlockDomain (domain) {
dispatch(openModal('CONFIRM', {
message: <FormattedMessage id='confirmations.domain_block.message' defaultMessage='Are you really, really sure you want to block the entire {domain}? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed.' values={{ domain: <strong>{domain}</strong> }} />,

View File

@ -12,6 +12,7 @@ import {
STATUS_UNMUTE_SUCCESS,
STATUS_REVEAL,
STATUS_HIDE,
STATUS_COLLAPSE,
} from '../actions/statuses';
import { TIMELINE_DELETE } from '../actions/timelines';
import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer';
@ -73,6 +74,8 @@ export default function statuses(state = initialState, action) {
}
});
});
case STATUS_COLLAPSE:
return state.setIn([action.id, 'collapsed'], action.isCollapsed);
case TIMELINE_DELETE:
return deleteStatus(state, action.id, action.references);
default:

View File

@ -4,9 +4,13 @@ import { FormattedNumber } from 'react-intl';
export const shortNumberFormat = number => {
if (number < 1000) {
return <FormattedNumber value={number} />;
} else if (number < 1000000) {
} else if (number < 10000) {
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={1} />K</Fragment>;
} else {
} else if (number < 1000000) {
return <Fragment><FormattedNumber value={number / 1000} maximumFractionDigits={0} />K</Fragment>;
} else if (number < 10000000) {
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={1} />M</Fragment>;
} else {
return <Fragment><FormattedNumber value={number / 1000000} maximumFractionDigits={0} />M</Fragment>;
}
};

View File

@ -6399,13 +6399,13 @@ noscript {
&__links {
font-size: 14px;
color: $darker-text-color;
padding: 10px 0;
a {
display: inline-block;
color: $darker-text-color;
text-decoration: none;
padding: 10px;
padding-top: 20px;
padding: 5px 10px;
font-weight: 500;
strong {

View File

@ -78,7 +78,7 @@ class SearchQueryTransformer < Parslet::Transform
elsif clause[:shortcode]
TermClause.new(prefix, operator, ":#{clause[:term]}:")
elsif clause[:phrase]
PhraseClause.new(prefix, operator, clause[:phrase].map { |p| p[:term].to_s }.join(' '))
PhraseClause.new(prefix, operator, clause[:phrase].is_a?(Array) ? clause[:phrase].map { |p| p[:term].to_s }.join(' ') : clause[:phrase].to_s)
else
raise "Unexpected clause type: #{clause}"
end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
# See: https://jamescrisp.org/2018/05/28/fixing-invalid-query-parameters-invalid-encoding-in-a-rails-app/
class HandleBadEncodingMiddleware
def initialize(app)
@app = app
end
def call(env)
begin
Rack::Utils.parse_nested_query(env['QUERY_STRING'].to_s)
rescue Rack::Utils::InvalidParameterError
env['QUERY_STRING'] = ''
end
@app.call(env)
end
end

View File

@ -50,7 +50,7 @@
class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[[:word:]\.\-]+[a-z0-9]+)?)/i
include AccountAssociations
include AccountAvatar
@ -168,6 +168,10 @@ class Account < ApplicationRecord
local? ? username : "#{username}@#{domain}"
end
def pretty_acct
local? ? username : "#{username}@#{Addressable::IDNA.to_unicode(domain)}"
end
def local_username_and_domain
"#{username}@#{Rails.configuration.x.local_domain}"
end

View File

@ -9,6 +9,7 @@ module Attachmentable
GIF_MATRIX_LIMIT = 921_600 # 1280x720px
included do
before_post_process :obfuscate_file_name
before_post_process :set_file_extensions
before_post_process :check_image_dimensions
before_post_process :set_file_content_type
@ -68,4 +69,14 @@ module Attachmentable
rescue Terrapin::CommandLineError
''
end
def obfuscate_file_name
self.class.attachment_definitions.each_key do |attachment_name|
attachment = send(attachment_name)
next if attachment.blank?
attachment.instance_write :file_name, SecureRandom.hex(8) + File.extname(attachment.instance_read(:file_name))
end
end
end

View File

@ -202,9 +202,12 @@ class MediaAttachment < ApplicationRecord
end
after_commit :reset_parent_cache, on: :update
before_create :prepare_description, unless: :local?
before_create :set_shortcode
before_post_process :set_type_and_extension
before_save :set_meta
class << self

View File

@ -24,6 +24,10 @@ class REST::AccountSerializer < ActiveModel::Serializer
object.id.to_s
end
def acct
object.pretty_acct
end
def note
Formatter.instance.simplified_format(object)
end

View File

@ -166,7 +166,7 @@ class BackupService < BaseService
io.write(buffer)
end
end
rescue Errno::ENOENT
rescue Errno::ENOENT, Seahorse::Client::NetworkingError
Rails.logger.warn "Could not backup file #{filename}: file not found"
end
end

View File

@ -14,7 +14,16 @@ class ProcessMentionsService < BaseService
mentions = []
status.text = status.text.gsub(Account::MENTION_RE) do |match|
username, domain = Regexp.last_match(1).split('@')
username, domain = Regexp.last_match(1).split('@')
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
mentioned_account = Account.find_remote(username, domain)
if mention_undeliverable?(mentioned_account)

View File

@ -12,6 +12,8 @@ class ResolveURLService < BaseService
process_local_url
elsif !fetched_resource.nil?
process_url
elsif @on_behalf_of.present?
process_url_from_db
end
end
@ -24,15 +26,19 @@ class ResolveURLService < BaseService
status = FetchRemoteStatusService.new.call(resource_url, body)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
elsif fetched_resource.nil? && @on_behalf_of.present?
# It may happen that the resource is a private toot, and thus not fetchable,
# but we can return the toot if we already know about it.
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
end
end
def process_url_from_db
# It may happen that the resource is a private toot, and thus not fetchable,
# but we can return the toot if we already know about it.
status = Status.find_by(uri: @url) || Status.find_by(url: @url)
authorize_with @on_behalf_of, status, :show? unless status.nil?
status
rescue Mastodon::NotPermittedError
nil
end
def fetched_resource
@fetched_resource ||= FetchResourceService.new.call(@url)
end

View File

@ -1,6 +1,7 @@
.batch-table__row
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
- if batch_available
%label.batch-table__row__select.batch-table__row__select--aligned.batch-checkbox
= f.check_box :tag_ids, { multiple: true, include_hidden: false }, tag.id
.directory__tag
= link_to admin_tag_path(tag.id) do

View File

@ -47,25 +47,26 @@
.batch-table.optional
.batch-table__toolbar
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
- if params[:pending_review] == '1'
- if params[:pending_review] == '1' || params[:unreviewed] == '1'
%label.batch-table__toolbar__select.batch-checkbox-all
= check_box_tag :batch_checkbox_all, nil, false
.batch-table__toolbar__actions
= f.button safe_join([fa_icon('check'), t('admin.accounts.approve')]), name: :approve, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
= f.button safe_join([fa_icon('times'), t('admin.accounts.reject')]), name: :reject, class: 'table-action-link', type: :submit, data: { confirm: t('admin.reports.are_you_sure') }
- else
- else
.batch-table__toolbar__actions
%span.neutral-hint= t('generic.no_batch_actions_available')
.batch-table__body
- if @tags.empty?
= nothing_here 'nothing-here--under-tabs'
- else
= render partial: 'tag', collection: @tags, locals: { f: f }
= render partial: 'tag', collection: @tags, locals: { f: f, batch_available: params[:pending_review] == '1' || params[:unreviewed] == '1' }
= paginate @tags
- if params[:pending_review] == '1'
- if params[:pending_review] == '1' || params[:unreviewed] == '1'
%hr.spacer/
%div{ style: 'overflow: hidden' }

View File

@ -5,6 +5,10 @@
.fields-group
= f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale, hint: false
- unless I18n.locale == :en
.flash-message{ style: "text-align: unset; color: unset" }
#{t 'appearance.localization.body'} #{content_tag(:a, t('appearance.localization.guide_link_text'), href: t('appearance.localization.guide_link'), target: "_blank", rel: "noopener", style: "text-decoration: underline")}
%h4= t 'appearance.advanced_web_interface'
%p.hint= t 'appearance.advanced_web_interface_hint'

View File

@ -1,3 +1,2 @@
- cache @status do
.activity-stream.activity-stream--headless
= render 'status', status: @status, centered: true, autoplay: @autoplay
.activity-stream.activity-stream--headless
= render 'status', status: @status, centered: true, autoplay: @autoplay

View File

@ -7,15 +7,18 @@ class RefollowWorker
def perform(target_account_id)
target_account = Account.find(target_account_id)
return unless target_account.protocol == :activitypub
return unless target_account.activitypub?
target_account.passive_relationships.where(account: Account.where(domain: nil)).includes(:account).reorder(nil).find_each do |follow|
reblogs = follow.show_reblogs?
target_account.followers.where(domain: nil).reorder(nil).find_each do |follower|
# Locally unfollow remote account
follower = follow.account
follower.unfollow!(target_account)
# Schedule re-follow
begin
FollowService.new.call(follower, target_account)
FollowService.new.call(follower, target_account, reblogs: reblogs)
rescue Mastodon::NotPermittedError, ActiveRecord::RecordNotFound, Mastodon::UnexpectedResponseError, HTTP::Error, OpenSSL::SSL::SSLError
next
end

View File

@ -7,6 +7,7 @@ require 'rails/all'
Bundler.require(*Rails.groups)
require_relative '../app/lib/exceptions'
require_relative '../app/middleware/handle_bad_encoding_middleware'
require_relative '../lib/paperclip/lazy_thumbnail'
require_relative '../lib/paperclip/gif_transcoder'
require_relative '../lib/paperclip/video_transcoder'
@ -118,6 +119,7 @@ module Mastodon
config.active_job.queue_adapter = :sidekiq
config.middleware.insert_before Rack::Runtime, HandleBadEncodingMiddleware
config.middleware.use Rack::Attack
config.middleware.use Rack::Deflater

View File

@ -8,20 +8,15 @@ Doorkeeper.configure do
end
resource_owner_from_credentials do |_routes|
if Devise.ldap_authentication
user = User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end
if Devise.pam_authentication
user ||= User.authenticate_with_ldap({ :email => request.params[:username], :password => request.params[:password] })
end
user = User.authenticate_with_ldap(email: request.params[:username], password: request.params[:password]) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(email: request.params[:username], password: request.params[:password]) if Devise.pam_authentication
if user.nil?
user = User.find_by(email: request.params[:username])
user = nil unless user.valid_password?(request.params[:password])
user = nil unless user&.valid_password?(request.params[:password])
end
user if !user&.otp_required_for_login?
user unless user&.otp_required_for_login?
end
# If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below.

View File

@ -1,5 +1,7 @@
# frozen_string_literal: true
Paperclip::DataUriAdapter.register
Paperclip.interpolates :filename do |attachment, style|
if style == :original
attachment.original_filename

View File

@ -46,10 +46,7 @@ class Rack::Attack
PROTECTED_PATHS_REGEX = Regexp.union(PROTECTED_PATHS.map { |path| /\A#{Regexp.escape(path)}/ })
# Always allow requests from localhost
# (blocklist & throttles are skipped)
Rack::Attack.safelist('allow from localhost') do |req|
# Requests are allowed if the return value is truthy
req.remote_ip == '127.0.0.1' || req.remote_ip == '::1'
end

View File

@ -594,6 +594,10 @@ en:
animations_and_accessibility: Animations and accessibility
confirmation_dialogs: Confirmation dialogs
discovery: Discovery
localization:
body: Mastodon is translated by volunteers.
guide_link: https://crowdin.com/project/mastodon
guide_link_text: Everyone can contribute.
sensitive_content: Sensitive content
toot_layout: Toot layout
application_mailer:

View File

@ -1,17 +1,19 @@
class FixNullBooleans < ActiveRecord::Migration[5.1]
def change
change_column_default :domain_blocks, :reject_media, false
change_column_null :domain_blocks, :reject_media, false, false
safety_assured do
change_column_default :domain_blocks, :reject_media, false
change_column_null :domain_blocks, :reject_media, false, false
change_column_default :imports, :approved, false
change_column_null :imports, :approved, false, false
change_column_default :imports, :approved, false
change_column_null :imports, :approved, false, false
change_column_null :statuses, :sensitive, false, false
change_column_null :statuses, :reply, false, false
change_column_null :statuses, :sensitive, false, false
change_column_null :statuses, :reply, false, false
change_column_null :users, :admin, false, false
change_column_null :users, :admin, false, false
change_column_default :users, :otp_required_for_login, false
change_column_null :users, :otp_required_for_login, false, false
change_column_default :users, :otp_required_for_login, false
change_column_null :users, :otp_required_for_login, false, false
end
end
end

View File

@ -1,6 +1,8 @@
class ChangeAccountsNonnullableInAccountModerationNotes < ActiveRecord::Migration[5.1]
def change
change_column_null :account_moderation_notes, :account_id, false
change_column_null :account_moderation_notes, :target_account_id, false
safety_assured do
change_column_null :account_moderation_notes, :account_id, false
change_column_null :account_moderation_notes, :target_account_id, false
end
end
end

View File

@ -1,5 +1,7 @@
class ChangeAccountIdNonnullableInLists < ActiveRecord::Migration[5.1]
def change
change_column_null :lists, :account_id, false
safety_assured do
change_column_null :lists, :account_id, false
end
end
end

View File

@ -1,8 +1,10 @@
class ChangeColumnsInNotificationsNonnullable < ActiveRecord::Migration[5.1]
def change
change_column_null :notifications, :activity_id, false
change_column_null :notifications, :activity_type, false
change_column_null :notifications, :account_id, false
change_column_null :notifications, :from_account_id, false
safety_assured do
change_column_null :notifications, :activity_id, false
change_column_null :notifications, :activity_type, false
change_column_null :notifications, :account_id, false
change_column_null :notifications, :from_account_id, false
end
end
end

View File

@ -60,7 +60,7 @@
},
"private": true,
"dependencies": {
"@babel/core": "^7.7.5",
"@babel/core": "^7.7.7",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-transform-react-inline-elements": "^7.7.4",
@ -69,7 +69,7 @@
"@babel/preset-react": "^7.7.4",
"@babel/runtime": "^7.7.7",
"@gamestdio/websocket": "^0.3.2",
"@clusterws/cws": "^0.16.0",
"@clusterws/cws": "^0.16.1",
"array-includes": "^3.1.1",
"atrament": "^0.2.3",
"arrow-key-navigation": "^1.1.0",
@ -107,7 +107,7 @@
"intl": "^1.2.5",
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^6.4.3",
"is-nan": "^1.2.1",
"is-nan": "^1.3.0",
"js-yaml": "^3.13.1",
"lodash": "^4.17.14",
"mark-loader": "^0.1.6",
@ -146,7 +146,7 @@
"react-textarea-autosize": "^7.1.2",
"react-toggle": "^4.1.1",
"redis": "^2.7.1",
"redux": "^4.0.4",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0",
"rellax": "^1.10.0",
@ -157,13 +157,13 @@
"sass-loader": "^8.0.0",
"stringz": "^2.0.0",
"substring-trie": "^1.0.2",
"terser-webpack-plugin": "^2.2.2",
"terser-webpack-plugin": "^2.3.1",
"tesseract.js": "^2.0.0-alpha.16",
"throng": "^4.0.0",
"tiny-queue": "^0.2.1",
"uuid": "^3.3.3",
"wavesurfer.js": "^3.2.0",
"webpack": "^4.41.2",
"webpack": "^4.41.5",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.10",
@ -175,7 +175,7 @@
"babel-jest": "^24.9.0",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.1",
"eslint": "^6.7.2",
"eslint": "^6.8.0",
"eslint-plugin-import": "~2.19.1",
"eslint-plugin-jsx-a11y": "~6.2.3",
"eslint-plugin-promise": "~4.2.1",
@ -183,7 +183,7 @@
"jest": "^24.9.0",
"raf": "^3.4.1",
"react-intl-translations-manager": "^5.0.3",
"react-test-renderer": "^16.11.0",
"react-test-renderer": "^16.12.0",
"sass-lint": "^1.13.1",
"webpack-dev-server": "^3.9.0",
"yargs": "^15.0.2"

View File

@ -85,10 +85,7 @@ describe Api::ProofsController do
end
it 'has the correct avatar url' do
first_part = 'https://cb6e6126.ngrok.io/system/accounts/avatars/'
last_part = 'original/avatar.gif'
expect(body_as_json[:avatar]).to match /#{Regexp.quote(first_part)}(?:\d{3,5}\/){3}#{Regexp.quote(last_part)}/
expect(body_as_json[:avatar]).to match "https://cb6e6126.ngrok.io#{alice.avatar.url}"
end
end
end

View File

@ -3,19 +3,38 @@ require 'rails_helper'
describe Api::V1::Accounts::FollowerAccountsController do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
before do
Fabricate(:follow, target_account: user.account)
alice.follow!(account)
bob.follow!(account)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: user.account.id, limit: 1 }
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
it 'returns accounts following the given account' do
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
it 'does not return blocked users' do
user.account.block!(bob)
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s
end
end
end

View File

@ -3,19 +3,38 @@ require 'rails_helper'
describe Api::V1::Accounts::FollowingAccountsController do
render_views
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read:accounts') }
let(:account) { Fabricate(:account) }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
before do
Fabricate(:follow, account: user.account)
account.follow!(alice)
account.follow!(bob)
allow(controller).to receive(:doorkeeper_token) { token }
end
describe 'GET #index' do
it 'returns http success' do
get :index, params: { account_id: user.account.id, limit: 1 }
get :index, params: { account_id: account.id, limit: 2 }
expect(response).to have_http_status(200)
end
it 'returns accounts followed by the given account' do
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
it 'does not return blocked users' do
user.account.block!(bob)
get :index, params: { account_id: account.id, limit: 2 }
expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s
end
end
end

View File

@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
context 'with an oauth token' do
before do
@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::FavouritedByAccountsController, type: :control
let(:status) { Fabricate(:status, account: user.account) }
before do
Fabricate(:favourite, status: status)
Favourite.create!(account: alice, status: status)
Favourite.create!(account: bob, status: status)
end
it 'returns http success' do
get :index, params: { status_id: status.id, limit: 1 }
get :index, params: { status_id: status.id, limit: 2 }
expect(response).to have_http_status(200)
expect(response.headers['Link'].links.size).to eq(2)
end
it 'returns accounts who favorited the status' do
get :index, params: { status_id: status.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
it 'does not return blocked users' do
user.account.block!(bob)
get :index, params: { status_id: status.id, limit: 2 }
expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s
end
end
end

View File

@ -6,6 +6,8 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
let(:user) { Fabricate(:user, account: Fabricate(:account, username: 'alice')) }
let(:app) { Fabricate(:application, name: 'Test app', website: 'http://testapp.com') }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, application: app, scopes: 'read:accounts') }
let(:alice) { Fabricate(:account) }
let(:bob) { Fabricate(:account) }
context 'with an oauth token' do
before do
@ -16,14 +18,28 @@ RSpec.describe Api::V1::Statuses::RebloggedByAccountsController, type: :controll
let(:status) { Fabricate(:status, account: user.account) }
before do
Fabricate(:status, reblog_of_id: status.id)
Fabricate(:status, account: alice, reblog_of_id: status.id)
Fabricate(:status, account: bob, reblog_of_id: status.id)
end
it 'returns http success' do
get :index, params: { status_id: status.id, limit: 1 }
get :index, params: { status_id: status.id, limit: 2 }
expect(response).to have_http_status(200)
expect(response.headers['Link'].links.size).to eq(2)
end
it 'returns accounts who reblogged the status' do
get :index, params: { status_id: status.id, limit: 2 }
expect(body_as_json.size).to eq 2
expect([body_as_json[0][:id], body_as_json[1][:id]]).to match_array([alice.id.to_s, bob.id.to_s])
end
it 'does not return blocked users' do
user.account.block!(bob)
get :index, params: { status_id: status.id, limit: 2 }
expect(body_as_json.size).to eq 1
expect(body_as_json[0][:id]).to eq alice.id.to_s
end
end
end

View File

@ -1,30 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe ApplicationController, type: :controller do
controller do
include ObfuscateFilename
obfuscate_filename :file
def file
render plain: params[:file]&.original_filename
end
end
before do
routes.draw { get 'file' => 'anonymous#file' }
end
it 'obfusticates filename if the given parameter is specified' do
file = fixture_file_upload('files/imports.txt', 'text/plain')
post 'file', params: { file: file }
expect(response.body).to end_with '.txt'
expect(response.body).not_to include 'imports'
end
it 'does nothing if the given parameter is not specified' do
post 'file'
end
end

View File

@ -22,6 +22,18 @@ describe FollowerAccountsController do
expect(assigned[0]).to eq follow1
expect(assigned[1]).to eq follow0
end
it 'does not assign blocked users' do
user = Fabricate(:user)
user.account.block!(follower0)
sign_in(user)
expect(response).to have_http_status(200)
assigned = assigns(:follows).to_a
expect(assigned.size).to eq 1
expect(assigned[0]).to eq follow1
end
end
context 'when format is json' do

View File

@ -22,6 +22,18 @@ describe FollowingAccountsController do
expect(assigned[0]).to eq follow1
expect(assigned[1]).to eq follow0
end
it 'does not assign blocked users' do
user = Fabricate(:user)
user.account.block!(followee0)
sign_in(user)
expect(response).to have_http_status(200)
assigned = assigns(:follows).to_a
expect(assigned.size).to eq 1
expect(assigned[0]).to eq follow1
end
end
context 'when format is json' do

View File

@ -1,47 +1,51 @@
require "rails_helper"
# frozen_string_literal: true
require 'rails_helper'
feature 'Log in' do
include ProfileStories
feature "Log in" do
given(:email) { "test@example.com" }
given(:password) { "password" }
given(:confirmed_at) { Time.zone.now }
background do
Fabricate(:user, email: email, password: password, confirmed_at: confirmed_at)
as_a_registered_user
visit new_user_session_path
end
subject { page }
scenario "A valid email and password user is able to log in" do
fill_in "user_email", with: email
fill_in "user_password", with: password
scenario 'A valid email and password user is able to log in' do
fill_in 'user_email', with: email
fill_in 'user_password', with: password
click_on I18n.t('auth.login')
is_expected.to have_css("div.app-holder")
is_expected.to have_css('div.app-holder')
end
scenario "A invalid email and password user is not able to log in" do
fill_in "user_email", with: "invalid_email"
fill_in "user_password", with: "invalid_password"
scenario 'A invalid email and password user is not able to log in' do
fill_in 'user_email', with: 'invalid_email'
fill_in 'user_password', with: 'invalid_password'
click_on I18n.t('auth.login')
is_expected.to have_css(".flash-message", text: failure_message("invalid"))
is_expected.to have_css('.flash-message', text: failure_message('invalid'))
end
context do
given(:confirmed_at) { nil }
scenario "A unconfirmed user is able to log in" do
fill_in "user_email", with: email
fill_in "user_password", with: password
scenario 'A unconfirmed user is able to log in' do
fill_in 'user_email', with: email
fill_in 'user_password', with: password
click_on I18n.t('auth.login')
is_expected.to have_css("div.admin-wrapper")
is_expected.to have_css('div.admin-wrapper')
end
end
def failure_message(message)
keys = User.authentication_keys.map { |key| User.human_attribute_name(key) }
I18n.t("devise.failure.#{message}", authentication_keys: keys.join("support.array.words_connector"))
I18n.t("devise.failure.#{message}", authentication_keys: keys.join('support.array.words_connector'))
end
end

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'rails_helper'
feature 'Profile' do
include ProfileStories
given(:local_domain) { ENV['LOCAL_DOMAIN'] }
background do
as_a_logged_in_user
with_alice_as_local_user
end
subject { page }
scenario 'I can view Annes public account' do
visit account_path('alice')
is_expected.to have_title("alice (@alice@#{local_domain})")
within('.public-account-header h1') do
is_expected.to have_content("alice @alice@#{local_domain}")
end
bio_elem = first('.public-account-bio')
expect(bio_elem).to have_content(alice_bio)
# The bio has hashtags made clickable
expect(bio_elem).to have_link('cryptology')
expect(bio_elem).to have_link('science')
# Nicknames are make clickable
expect(bio_elem).to have_link('@alice')
expect(bio_elem).to have_link('@bob')
# Nicknames not on server are not clickable
expect(bio_elem).not_to have_link('@pepe')
end
scenario 'I can change my account' do
visit settings_profile_path
fill_in 'Display name', with: 'Bob'
fill_in 'Bio', with: 'Bob is silent'
click_on 'Save changes'
is_expected.to have_content 'Changes successfully saved!'
# View my own public profile and see the changes
click_link "Bob @bob@#{local_domain}"
within('.public-account-header h1') do
is_expected.to have_content("Bob @bob@#{local_domain}")
end
expect(first('.public-account-bio')).to have_content('Bob is silent')
end
end

View File

@ -0,0 +1,21 @@
require 'rails_helper'
RSpec.describe HandleBadEncodingMiddleware do
let(:app) { double() }
let(:middleware) { HandleBadEncodingMiddleware.new(app) }
it "request with query string is unchanged" do
expect(app).to receive(:call).with("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
middleware.call("PATH" => "/some/path", "QUERY_STRING" => "name=fred")
end
it "request with no query string is unchanged" do
expect(app).to receive(:call).with("PATH" => "/some/path")
middleware.call("PATH" => "/some/path")
end
it "request with invalid encoding in query string drops query string" do
expect(app).to receive(:call).with("QUERY_STRING" => "", "PATH" => "/some/path")
middleware.call("QUERY_STRING" => "q=%2Fsearch%2Fall%Forder%3Ddescending%26page%3D5%26sort%3Dcreated_at", "PATH" => "/some/path")
end
end

View File

@ -823,4 +823,5 @@ RSpec.describe Account, type: :model do
end
include_examples 'AccountAvatar', :account
include_examples 'AccountHeader', :account
end

View File

@ -133,6 +133,24 @@ RSpec.describe MediaAttachment, type: :model do
expect(media.file.meta["small"]["height"]).to eq 327
expect(media.file.meta["small"]["aspect"]).to eq 490.0 / 327
end
it 'gives the file a random name' do
expect(media.file_file_name).to_not eq 'attachment.jpg'
end
end
describe 'base64-encoded jpeg' do
let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
let(:media) { MediaAttachment.create(account: Fabricate(:account), file: base64_attachment) }
it 'saves media attachment' do
expect(media.persisted?).to be true
expect(media.file).to_not be_nil
end
it 'gives the file a file name' do
expect(media.file_file_name).to_not be_blank
end
end
describe 'descriptions for remote attachments' do

View File

@ -5,11 +5,11 @@ RSpec.describe ProcessMentionsService, type: :service do
let(:visibility) { :public }
let(:status) { Fabricate(:status, account: account, text: "Hello @#{remote_user.acct}", visibility: visibility) }
subject { ProcessMentionsService.new }
context 'OStatus with public toot' do
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
subject { ProcessMentionsService.new }
before do
stub_request(:post, remote_user.salmon_url)
subject.call(status)
@ -24,8 +24,6 @@ RSpec.describe ProcessMentionsService, type: :service do
let(:visibility) { :private }
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :ostatus, domain: 'example.com', salmon_url: 'http://salmon.example.com') }
subject { ProcessMentionsService.new }
before do
stub_request(:post, remote_user.salmon_url)
subject.call(status)
@ -41,29 +39,45 @@ RSpec.describe ProcessMentionsService, type: :service do
end
context 'ActivityPub' do
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
context do
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
subject { ProcessMentionsService.new }
before do
stub_request(:post, remote_user.inbox_url)
subject.call(status)
end
before do
stub_request(:post, remote_user.inbox_url)
subject.call(status)
it 'creates a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 1
end
it 'sends activity to the inbox' do
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
end
end
it 'creates a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 1
end
context 'with an IDN domain' do
let(:remote_user) { Fabricate(:account, username: 'sneak', protocol: :activitypub, domain: 'xn--hresiar-mxa.ch', inbox_url: 'http://example.com/inbox') }
let(:status) { Fabricate(:status, account: account, text: "Hello @sneak@hæresiar.ch") }
it 'sends activity to the inbox' do
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
before do
stub_request(:post, remote_user.inbox_url)
subject.call(status)
end
it 'creates a mention' do
expect(remote_user.mentions.where(status: status).count).to eq 1
end
it 'sends activity to the inbox' do
expect(a_request(:post, remote_user.inbox_url)).to have_been_made.once
end
end
end
context 'Temporarily-unreachable ActivityPub user' do
let(:remote_user) { Fabricate(:account, username: 'remote_user', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox', last_webfingered_at: nil) }
subject { ProcessMentionsService.new }
before do
stub_request(:get, "https://example.com/.well-known/host-meta").to_return(status: 404)
stub_request(:get, "https://example.com/.well-known/webfinger?resource=acct:remote_user@example.com").to_return(status: 500)

View File

@ -16,4 +16,24 @@ shared_examples 'AccountAvatar' do |fabricator|
end
end
end
describe 'base64-encoded files' do
let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
let(:account) { Fabricate(fabricator, avatar: base64_attachment) }
it 'saves avatar' do
expect(account.persisted?).to be true
expect(account.avatar).to_not be_nil
end
it 'gives the avatar a file name' do
expect(account.avatar_file_name).to_not be_blank
end
it 'saves a new avatar under a different file name' do
previous_file_name = account.avatar_file_name
account.update(avatar: base64_attachment)
expect(account.avatar_file_name).to_not eq previous_file_name
end
end
end

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
shared_examples 'AccountHeader' do |fabricator|
describe 'base64-encoded files' do
let(:base64_attachment) { "data:image/jpeg;base64,#{Base64.encode64(attachment_fixture('attachment.jpg').read)}" }
let(:account) { Fabricate(fabricator, header: base64_attachment) }
it 'saves header' do
expect(account.persisted?).to be true
expect(account.header).to_not be_nil
end
it 'gives the header a file name' do
expect(account.header_file_name).to_not be_blank
end
it 'saves a new header under a different file name' do
previous_file_name = account.header_file_name
account.update(header: base64_attachment)
expect(account.header_file_name).to_not eq previous_file_name
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module ProfileStories
attr_reader :bob, :alice, :alice_bio
def as_a_registered_user
@bob = Fabricate(
:user,
email: email, password: password, confirmed_at: confirmed_at,
account: Fabricate(:account, username: 'bob')
)
end
def as_a_logged_in_user
as_a_registered_user
visit new_user_session_path
fill_in 'user_email', with: email
fill_in 'user_password', with: password
click_on I18n.t('auth.login')
end
def with_alice_as_local_user
@alice_bio = '@alice and @bob are fictional characters commonly used as'\
'placeholder names in #cryptology, as well as #science and'\
'engineering 📖 literature. Not affilated with @pepe.'
@alice = Fabricate(
:user,
email: 'alice@example.com', password: password, confirmed_at: confirmed_at,
account: Fabricate(:account, username: 'alice', note: @alice_bio)
)
end
def confirmed_at
@confirmed_at ||= Time.zone.now
end
def email
@email ||= 'test@example.com'
end
def password
@password ||= 'password'
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'rails_helper'
describe RefollowWorker do
subject { described_class.new }
let(:account) { Fabricate(:account, domain: 'example.org', protocol: :activitypub) }
let(:alice) { Fabricate(:account, domain: nil, username: 'alice') }
let(:bob) { Fabricate(:account, domain: nil, username: 'bob') }
describe 'perform' do
let(:service) { double }
before do
allow(FollowService).to receive(:new).and_return(service)
allow(service).to receive(:call)
alice.follow!(account, reblogs: true)
bob.follow!(account, reblogs: false)
end
it 'calls FollowService for local followers' do
result = subject.perform(account.id)
expect(result).to be_nil
expect(service).to have_received(:call).with(alice, account, reblogs: true)
expect(service).to have_received(:call).with(bob, account, reblogs: false)
end
end
end

146
yarn.lock
View File

@ -9,15 +9,15 @@
dependencies:
"@babel/highlight" "^7.0.0"
"@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.7.5":
version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.5.tgz#ae1323cd035b5160293307f50647e83f8ba62f7e"
integrity sha512-M42+ScN4+1S9iB6f+TL7QBpoQETxbclx+KNoKJABghnKYE+fMzSGqst0BZJc8CpI625bwPwYgUyRvxZ+0mZzpw==
"@babel/core@^7.1.0", "@babel/core@^7.4.5", "@babel/core@^7.7.7":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.7.7.tgz#ee155d2e12300bcc0cff6a8ad46f2af5063803e9"
integrity sha512-jlSjuj/7z138NLZALxVgrx13AOtqip42ATZP7+kYl53GvDV6+4dCek1mVUo8z8c8Xnw/mx2q3d9HWh3griuesQ==
dependencies:
"@babel/code-frame" "^7.5.5"
"@babel/generator" "^7.7.4"
"@babel/generator" "^7.7.7"
"@babel/helpers" "^7.7.4"
"@babel/parser" "^7.7.5"
"@babel/parser" "^7.7.7"
"@babel/template" "^7.7.4"
"@babel/traverse" "^7.7.4"
"@babel/types" "^7.7.4"
@ -29,10 +29,10 @@
semver "^5.4.1"
source-map "^0.5.0"
"@babel/generator@^7.0.0", "@babel/generator@^7.7.4":
version "7.7.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.4.tgz#db651e2840ca9aa66f327dcec1dc5f5fa9611369"
integrity sha512-m5qo2WgdOJeyYngKImbkyQrnUN1mPceaG5BV+G0E3gWsa4l/jCSryWJdM2x8OuGAOyh+3d5pVYfZWCiNFtynxg==
"@babel/generator@^7.0.0", "@babel/generator@^7.7.4", "@babel/generator@^7.7.7":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.7.7.tgz#859ac733c44c74148e1a72980a64ec84b85f4f45"
integrity sha512-/AOIBpHh/JU1l0ZFS4kiRCBnLi6OTHzh0RPk3h9isBxkkqELtQNFi1Vr/tiG9p1yfoUdKVwISuXWQR+hwwM4VQ==
dependencies:
"@babel/types" "^7.7.4"
jsesc "^2.5.1"
@ -252,10 +252,10 @@
esutils "^2.0.2"
js-tokens "^4.0.0"
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.5":
version "7.7.5"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.5.tgz#cbf45321619ac12d83363fcf9c94bb67fa646d71"
integrity sha512-KNlOe9+/nk4i29g0VXgl8PEXIRms5xKLJeuZ6UptN0fHv+jDiriG+y94X6qAgWTR0h3KaoM1wK5G5h7MHFRSig==
"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.7.4", "@babel/parser@^7.7.7":
version "7.7.7"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.7.7.tgz#1b886595419cf92d811316d5b715a53ff38b4937"
integrity sha512-WtTZMZAZLbeymhkd/sEaPD8IQyGAhmuTuvTzLiCFM7iXiVdY0gc0IaI+cW0fh1BnSMbJSzXX6/fHllgHKwHhXw==
"@babel/plugin-proposal-async-generator-functions@^7.7.4":
version "7.7.4"
@ -795,10 +795,10 @@
lodash "^4.17.13"
to-fast-properties "^2.0.0"
"@clusterws/cws@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.16.0.tgz#f6116cbf3a8b7ad0657916616ce5f8248746b797"
integrity sha512-YeGpAPIdkBsOnAkmFKVMWEjCKDH900U2if0B+nc1imfv+64AIb2JX2xiTA6BLDLppEgWV5c6bpWESjbHCNblHw==
"@clusterws/cws@^0.16.1":
version "0.16.1"
resolved "https://registry.yarnpkg.com/@clusterws/cws/-/cws-0.16.1.tgz#443c47909601e87b881ffb2c4c83cbf27a228487"
integrity sha512-OamMsXwVppfmwX14Ed1msJJN0KNi+VBQh5AkAqUvIGTcJyHcjpsKjU15wS8QumvEewxx9gvGp7aSDop0/R5Gqg==
"@cnakazawa/watch@^1.0.3":
version "1.0.3"
@ -3247,7 +3247,7 @@ default-gateway@^4.2.0:
execa "^1.0.0"
ip-regex "^2.1.0"
define-properties@^1.1.1, define-properties@^1.1.2, define-properties@^1.1.3:
define-properties@^1.1.2, define-properties@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1"
integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==
@ -3942,10 +3942,10 @@ eslint@^2.7.0:
text-table "~0.2.0"
user-home "^2.0.0"
eslint@^6.7.2:
version "6.7.2"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.7.2.tgz#c17707ca4ad7b2d8af986a33feba71e18a9fecd1"
integrity sha512-qMlSWJaCSxDFr8fBPvJM9kJwbazrhNcBU3+DszDW1OlEwKBBRWsJc7NJFelvwQpanHCR14cOLD41x8Eqvo3Nng==
eslint@^6.8.0:
version "6.8.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
dependencies:
"@babel/code-frame" "^7.0.0"
ajv "^6.10.0"
@ -4031,12 +4031,7 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
esutils@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=
esutils@^2.0.2:
esutils@^2.0.0, esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
@ -4385,10 +4380,10 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0:
make-dir "^2.0.0"
pkg-dir "^3.0.0"
find-cache-dir@^3.0.0, find-cache-dir@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.1.0.tgz#9935894999debef4cf9f677fdf646d002c4cdecb"
integrity sha512-zw+EFiNBNPgI2NTrKkDd1xd7q0cs6wr/iWnr/oUkI0yF9K9GqQ+riIt4aiyFaaqpaWbxPrJXHI+QvmNUQbX+0Q==
find-cache-dir@^3.0.0, find-cache-dir@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874"
integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg==
dependencies:
commondir "^1.0.1"
make-dir "^3.0.0"
@ -5620,12 +5615,12 @@ is-my-json-valid@^2.10.0:
jsonpointer "^4.0.0"
xtend "^4.0.0"
is-nan@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2"
integrity sha1-n69ltvttskt/XAYoR16nH5iEAeI=
is-nan@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03"
integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ==
dependencies:
define-properties "^1.1.1"
define-properties "^1.1.3"
is-number-object@^1.0.4:
version "1.0.4"
@ -8785,15 +8780,15 @@ react-swipeable-views@^0.13.3:
react-swipeable-views-utils "^0.13.3"
warning "^4.0.1"
react-test-renderer@^16.0.0-0, react-test-renderer@^16.11.0:
version "16.11.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.11.0.tgz#72574566496462c808ac449b0287a4c0a1a7d8f8"
integrity sha512-nh9gDl8R4ut+ZNNb2EeKO5VMvTKxwzurbSMuGBoKtjpjbg8JK/u3eVPVNi1h1Ue+eYK9oSzJjb+K3lzLxyA4ag==
react-test-renderer@^16.0.0-0, react-test-renderer@^16.12.0:
version "16.12.0"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.12.0.tgz#11417ffda579306d4e841a794d32140f3da1b43f"
integrity sha512-Vj/teSqt2oayaWxkbhQ6gKis+t5JrknXfPVo+aIJ8QwYAqMPH77uptOdrlphyxl8eQI/rtkOYg86i/UWkpFu0w==
dependencies:
object-assign "^4.1.1"
prop-types "^15.6.2"
react-is "^16.8.6"
scheduler "^0.17.0"
scheduler "^0.18.0"
react-textarea-autosize@^7.1.2:
version "7.1.2"
@ -8946,10 +8941,10 @@ redux-thunk@^2.2.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
redux@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
@ -9395,14 +9390,6 @@ sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.17.0:
version "0.17.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.17.0.tgz#7c9c673e4ec781fac853927916d1c426b6f3ddfe"
integrity sha512-7rro8Io3tnCPuY4la/NuI5F2yfESpnfZyT6TtkXnSWVkcu0BCDJ+8gk5ozUaFaxpIyNuWAPXrH0yFcSi28fnDA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
@ -9487,12 +9474,7 @@ send@0.17.1:
range-parser "~1.2.1"
statuses "~1.5.0"
serialize-javascript@^1.7.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.9.1.tgz#cfc200aef77b600c47da9bb8149c943e798c2fdb"
integrity sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==
serialize-javascript@^2.1.1, serialize-javascript@^2.1.2:
serialize-javascript@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61"
integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==
@ -10164,39 +10146,39 @@ tcomb@^2.5.0:
resolved "https://registry.yarnpkg.com/tcomb/-/tcomb-2.7.0.tgz#10d62958041669a5d53567b9a4ee8cde22b1c2b0"
integrity sha1-ENYpWAQWaaXVNWe5pO6M3iKxwrA=
terser-webpack-plugin@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4"
integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==
terser-webpack-plugin@^1.4.3:
version "1.4.3"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz#5ecaf2dbdc5fb99745fd06791f46fc9ddb1c9a7c"
integrity sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==
dependencies:
cacache "^12.0.2"
find-cache-dir "^2.1.0"
is-wsl "^1.1.0"
schema-utils "^1.0.0"
serialize-javascript "^1.7.0"
serialize-javascript "^2.1.2"
source-map "^0.6.1"
terser "^4.1.2"
webpack-sources "^1.4.0"
worker-farm "^1.7.0"
terser-webpack-plugin@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.2.2.tgz#2a6e00237125564a455ad69b22e08ee59420473a"
integrity sha512-/CHMNswPMAwuD2kd++qys8UmBRmsshPSzHw4BlDwurPtK9YjeK93OV89YWkJulHk972cs07K/7Z92V6PNjWF8A==
terser-webpack-plugin@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.1.tgz#6a63c27debc15b25ffd2588562ee2eeabdcab923"
integrity sha512-dNxivOXmDgZqrGxOttBH6B4xaxT4zNC+Xd+2K8jwGDMK5q2CZI+KZMA1AAnSRT+BTRvuzKsDx+fpxzPAmAMVcA==
dependencies:
cacache "^13.0.1"
find-cache-dir "^3.1.0"
find-cache-dir "^3.2.0"
jest-worker "^24.9.0"
schema-utils "^2.6.1"
serialize-javascript "^2.1.1"
serialize-javascript "^2.1.2"
source-map "^0.6.1"
terser "^4.4.2"
terser "^4.4.3"
webpack-sources "^1.4.3"
terser@^4.1.2, terser@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.2.tgz#448fffad0245f4c8a277ce89788b458bfd7706e8"
integrity sha512-Uufrsvhj9O1ikwgITGsZ5EZS6qPokUOkCegS7fYOdGTv+OA90vndUbU6PEjr5ePqHfNUbGyMO7xyIZv2MhsALQ==
terser@^4.1.2, terser@^4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/terser/-/terser-4.4.3.tgz#401abc52b88869cf904412503b1eb7da093ae2f0"
integrity sha512-0ikKraVtRDKGzHrzkCv5rUNDzqlhmhowOBqC0XqUHFpW+vJ45+20/IFBcebwKfiS2Z9fJin6Eo+F1zLZsxi8RA==
dependencies:
commander "^2.20.0"
source-map "~0.6.1"
@ -10858,10 +10840,10 @@ webpack-sources@^1.0.0, webpack-sources@^1.0.1, webpack-sources@^1.1.0, webpack-
source-list-map "^2.0.0"
source-map "~0.6.1"
webpack@^4.41.2:
version "4.41.2"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.2.tgz#c34ec76daa3a8468c9b61a50336d8e3303dce74e"
integrity sha512-Zhw69edTGfbz9/8JJoyRQ/pq8FYUoY0diOXqW0T6yhgdhCv6wr0hra5DwwWexNRns2Z2+gsnrNcbe9hbGBgk/A==
webpack@^4.41.5:
version "4.41.5"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.41.5.tgz#3210f1886bce5310e62bb97204d18c263341b77c"
integrity sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==
dependencies:
"@webassemblyjs/ast" "1.8.5"
"@webassemblyjs/helper-module-context" "1.8.5"
@ -10883,7 +10865,7 @@ webpack@^4.41.2:
node-libs-browser "^2.2.1"
schema-utils "^1.0.0"
tapable "^1.1.3"
terser-webpack-plugin "^1.4.1"
terser-webpack-plugin "^1.4.3"
watchpack "^1.6.0"
webpack-sources "^1.4.1"