Verified Commit 4b8b31c4 authored by Morgan Bazalgette's avatar Morgan Bazalgette

Merge branch 'master' of github.com:tootsuite/mastodon

parents fa5858c4 7e0aed39
# Federation
LOCAL_DOMAIN=cb6e6126.ngrok.io
LOCAL_HTTPS=true
# test pam authentication
PAM_ENABLED=true
PAM_DEFAULT_SERVICE=pam_test
PAM_CONTROLLED_SERVICE=pam_test_controlled
......@@ -23,6 +23,7 @@ env:
- RAILS_ENV=test
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
- PARALLEL_TEST_PROCESSORS=2
- ALLOW_NOPAM=true
addons:
postgresql: 9.4
......@@ -47,7 +48,7 @@ services:
install:
- nvm install
- bundle install --path=vendor/bundle --without development production --retry=3 --jobs=16
- bundle install --path=vendor/bundle --with pam_authentication --without development production --retry=3 --jobs=16
- yarn install
before_script:
......
......@@ -5,12 +5,12 @@ ruby '>= 2.3.0', '< 2.6.0'
gem 'pkg-config', '~> 1.2'
gem 'puma', '~> 3.10'
gem 'rails', '~> 5.1.4'
gem 'puma', '~> 3.11'
gem 'rails', '~> 5.2.0'
gem 'hamlit-rails', '~> 0.2'
gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7'
gem 'pg', '~> 1.0'
gem 'pghero', '~> 2.1'
gem 'dotenv-rails', '~> 2.2'
gem 'aws-sdk-s3', '~> 1.8', require: false
......@@ -23,17 +23,17 @@ gem 'streamio-ffmpeg', '~> 3.0'
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.5'
gem 'bootsnap'
gem 'bootsnap', '~> 1.3'
gem 'browser'
gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.0'
gem 'devise', '~> 4.4'
gem 'devise-two-factor', '~> 3.0'
gem 'devise-two-factor', '~> 3.0', git: 'https://github.com/ykzts/devise-two-factor.git', branch: 'rails-5.2'
group :pam_authentication, optional: true do
gem 'devise_pam_authenticatable2', '~> 9.0'
gem 'devise_pam_authenticatable2', '~> 9.1'
end
gem 'net-ldap', '~> 0.10'
......@@ -41,7 +41,7 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.2'
gem 'doorkeeper', '~> 4.2'
gem 'doorkeeper', '~> 4.3'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
gem 'goldfinger', '~> 2.1'
......@@ -50,50 +50,50 @@ gem 'redis-namespace', '~> 1.5'
gem 'htmlentities', '~> 4.3'
gem 'http', '~> 3.0'
gem 'http_accept_language', '~> 2.1'
gem 'httplog', '~> 0.99'
gem 'httplog', '~> 1.0'
gem 'idn-ruby', require: 'idn'
gem 'kaminari', '~> 1.1'
gem 'link_header', '~> 0.0'
gem 'mime-types', '~> 3.1'
gem 'nokogiri', '~> 1.8'
gem 'nsa', '~> 0.2'
gem 'oj', '~> 3.3'
gem 'oj', '~> 3.4'
gem 'ostatus2', '~> 2.0'
gem 'ox', '~> 2.8'
gem 'pundit', '~> 1.1'
gem 'premailer-rails'
gem 'rack-attack', '~> 5.0'
gem 'rack-cors', '~> 0.4', require: 'rack/cors'
gem 'rack-attack', '~> 5.2'
gem 'rack-cors', '~> 1.0', require: 'rack/cors'
gem 'rack-timeout', '~> 0.4'
gem 'rails-i18n', '~> 5.0'
gem 'rails-i18n', '~> 5.1'
gem 'rails-settings-cached', '~> 0.6'
gem 'redis', '~> 3.3', require: ['redis', 'redis/connection/hiredis']
gem 'redis', '~> 4.0', require: ['redis', 'redis/connection/hiredis']
gem 'mario-redis-lock', '~> 1.2', require: 'redis_lock'
gem 'rqrcode', '~> 0.10'
gem 'ruby-oembed', '~> 0.12', require: 'oembed'
gem 'ruby-progressbar', '~> 1.4'
gem 'sanitize', '~> 4.6.4'
gem 'sidekiq', '~> 5.0'
gem 'sidekiq-scheduler', '~> 2.1'
gem 'sanitize', '~> 4.6'
gem 'sidekiq', '~> 5.1'
gem 'sidekiq-scheduler', '~> 2.2'
gem 'sidekiq-unique-jobs', '~> 5.0'
gem 'sidekiq-bulk', '~>0.1.1'
gem 'simple-navigation', '~> 4.0'
gem 'simple_form', '~> 3.4'
gem 'simple_form', '~> 4.0'
gem 'sprockets-rails', '~> 3.2', require: 'sprockets/railtie'
gem 'stoplight', '~> 2.1.3'
gem 'strong_migrations'
gem 'strong_migrations', '~> 0.2'
gem 'tty-command'
gem 'tty-prompt'
gem 'twitter-text', '~> 1.14'
gem 'tzinfo-data', '~> 1.2017'
gem 'webpacker', '~> 3.0'
gem 'tzinfo-data', '~> 1.2018'
gem 'webpacker', '~> 3.4'
gem 'webpush'
gem 'json-ld-preloaded', '~> 2.2.1'
gem 'rdf-normalize', '~> 0.3.1'
gem 'json-ld-preloaded', '~> 2.2'
gem 'rdf-normalize', '~> 0.3'
group :development, :test do
gem 'fabrication', '~> 2.18'
gem 'fabrication', '~> 2.20'
gem 'fuubar', '~> 2.2'
gem 'i18n-tasks', '~> 0.9', require: false
gem 'pry-rails', '~> 0.3'
......@@ -105,15 +105,15 @@ group :production, :test do
end
group :test do
gem 'capybara', '~> 2.15'
gem 'capybara', '~> 2.18'
gem 'climate_control', '~> 0.2'
gem 'faker', '~> 1.7'
gem 'faker', '~> 1.8'
gem 'microformats', '~> 4.0'
gem 'rails-controller-testing', '~> 1.0'
gem 'rspec-sidekiq', '~> 3.0'
gem 'simplecov', '~> 0.14', require: false
gem 'webmock', '~> 3.0'
gem 'parallel_tests', '~> 2.17'
gem 'webmock', '~> 3.3'
gem 'parallel_tests', '~> 2.21'
end
group :development do
......@@ -121,12 +121,12 @@ group :development do
gem 'annotate', '~> 2.7'
gem 'better_errors', '~> 2.4'
gem 'binding_of_caller', '~> 0.7'
gem 'bullet', '~> 5.5'
gem 'bullet', '~> 5.7'
gem 'letter_opener', '~> 1.4'
gem 'letter_opener_web', '~> 1.3'
gem 'memory_profiler'
gem 'rubocop', require: false
gem 'brakeman', '~> 4.0', require: false
gem 'brakeman', '~> 4.2', require: false
gem 'bundler-audit', '~> 0.6', require: false
gem 'scss_lint', '~> 0.55', require: false
......@@ -137,6 +137,6 @@ group :development do
end
group :production do
gem 'lograge', '~> 0.7'
gem 'lograge', '~> 0.9'
gem 'redis-rails', '~> 5.0'
end
This diff is collapsed.
# frozen_string_literal: true
module Admin
class ChangeEmailsController < BaseController
before_action :set_account
before_action :require_local_account!
def show
authorize @user, :change_email?
end
def update
authorize @user, :change_email?
new_email = resource_params.fetch(:unconfirmed_email)
if new_email != @user.email
@user.update!(
unconfirmed_email: new_email,
# Regenerate the confirmation token:
confirmation_token: nil
)
log_action :change_email, @user
@user.send_confirmation_instructions
end
redirect_to admin_account_path(@account.id), notice: I18n.t('admin.accounts.change_email.changed_msg')
end
private
def set_account
@account = Account.find(params[:account_id])
@user = @account.user
end
def require_local_account!
redirect_to admin_account_path(@account.id) unless @account.local? && @account.user.present?
end
def resource_params
params.require(:user).permit(
:unconfirmed_email
)
end
end
end
......@@ -8,19 +8,26 @@ module Admin
authorize ReportNote, :create?
@report_note = current_account.report_notes.new(resource_params)
@report = @report_note.report
if @report_note.save
if params[:create_and_resolve]
@report_note.report.update!(action_taken: true, action_taken_by_account_id: current_account.id)
log_action :resolve, @report_note.report
@report.resolve!(current_account)
log_action :resolve, @report
redirect_to admin_reports_path, notice: I18n.t('admin.reports.resolved_msg')
else
redirect_to admin_report_path(@report_note.report_id), notice: I18n.t('admin.report_notes.created_msg')
return
end
if params[:create_and_unresolve]
@report.unresolve!
log_action :reopen, @report
end
redirect_to admin_report_path(@report), notice: I18n.t('admin.report_notes.created_msg')
else
@report = @report_note.report
@report_notes = @report.notes.latest
@report_history = @report.history
@form = Form::StatusBatch.new
render template: 'admin/reports/show'
......
......@@ -13,6 +13,7 @@ module Admin
authorize @report, :show?
@report_note = @report.notes.new
@report_notes = @report.notes.latest
@report_history = @report.history
@form = Form::StatusBatch.new
end
......@@ -38,36 +39,33 @@ module Admin
@report.update!(assigned_account_id: nil)
log_action :unassigned, @report
when 'reopen'
@report.update!(action_taken: false, action_taken_by_account_id: nil)
@report.unresolve!
log_action :reopen, @report
when 'resolve'
@report.update!(action_taken_by_current_attributes)
@report.resolve!(current_account)
log_action :resolve, @report
when 'suspend'
Admin::SuspensionWorker.perform_async(@report.target_account.id)
log_action :resolve, @report
log_action :suspend, @report.target_account
resolve_all_target_account_reports
@report.reload
when 'silence'
@report.target_account.update!(silenced: true)
log_action :resolve, @report
log_action :silence, @report.target_account
resolve_all_target_account_reports
@report.reload
else
raise ActiveRecord::RecordNotFound
end
end
def action_taken_by_current_attributes
{ action_taken: true, action_taken_by_account_id: current_account.id }
@report.reload
end
def resolve_all_target_account_reports
unresolved_reports_for_target_account.update_all(
action_taken_by_current_attributes
)
unresolved_reports_for_target_account.update_all(action_taken: true, action_taken_by_account_id: current_account.id)
end
def unresolved_reports_for_target_account
......
......@@ -17,7 +17,7 @@ class Api::V1::StatusesController < Api::BaseController
end
def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(current_account)
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(DEFAULT_STATUSES_LIMIT, current_account)
descendants_results = @status.descendants(current_account)
loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status)
......
......@@ -7,9 +7,6 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
protect_from_forgery with: :exception
def create
params.require(:subscription).require(:endpoint)
params.require(:subscription).require(:keys).require([:auth, :p256dh])
active_session = current_session
unless active_session.web_push_subscription.nil?
......@@ -29,12 +26,12 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
},
}
data.deep_merge!(params[:data]) if params[:data]
data.deep_merge!(data_params) if params[:data]
web_subscription = ::Web::PushSubscription.create!(
endpoint: params[:subscription][:endpoint],
key_p256dh: params[:subscription][:keys][:p256dh],
key_auth: params[:subscription][:keys][:auth],
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
data: data
)
......@@ -44,12 +41,22 @@ class Api::Web::PushSubscriptionsController < Api::BaseController
end
def update
params.require([:id, :data])
params.require([:id])
web_subscription = ::Web::PushSubscription.find(params[:id])
web_subscription.update!(data: params[:data])
web_subscription.update!(data: data_params)
render json: web_subscription.as_payload
end
private
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
end
def data_params
@data_params ||= params.require(:data).permit(:alerts)
end
end
......@@ -9,7 +9,7 @@ class Settings::FollowerDomainsController < ApplicationController
def show
@account = current_account
@domains = current_account.followers.reorder('MIN(follows.id) DESC').group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
@domains = current_account.followers.reorder(Arel.sql('MIN(follows.id) DESC')).group('accounts.domain').select('accounts.domain, count(accounts.id) as accounts_from_domain').page(params[:page]).per(10)
end
def update
......
......@@ -11,7 +11,9 @@ class Settings::ProfilesController < ApplicationController
obfuscate_filename [:account, :avatar]
obfuscate_filename [:account, :header]
def show; end
def show
@account.build_fields
end
def update
if UpdateAccountService.new.call(@account, account_params)
......@@ -25,7 +27,7 @@ class Settings::ProfilesController < ApplicationController
private
def account_params
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked)
params.require(:account).permit(:display_name, :note, :avatar, :header, :locked, fields_attributes: [:name, :value])
end
def set_account
......
......@@ -4,6 +4,8 @@ class StatusesController < ApplicationController
include SignatureAuthentication
include Authorization
ANCESTORS_LIMIT = 20
layout 'public'
before_action :set_account
......@@ -16,8 +18,9 @@ class StatusesController < ApplicationController
def show
respond_to do |format|
format.html do
@ancestors = @status.reply? ? cache_collection(@status.ancestors(current_account), Status) : []
@descendants = cache_collection(@status.descendants(current_account), Status)
@ancestors = @status.reply? ? cache_collection(@status.ancestors(ANCESTORS_LIMIT, current_account), Status) : []
@next_ancestor = @ancestors.size < ANCESTORS_LIMIT ? nil : @ancestors.shift
@descendants = cache_collection(@status.descendants(current_account), Status)
render 'stream_entries/show'
end
......
......@@ -15,8 +15,7 @@ class StreamEntriesController < ApplicationController
def show
respond_to do |format|
format.html do
@ancestors = @stream_entry.activity.reply? ? cache_collection(@stream_entry.activity.ancestors(current_account), Status) : []
@descendants = cache_collection(@stream_entry.activity.descendants(current_account), Status)
redirect_to short_account_status_url(params[:account_username], @stream_entry.activity) if @type == 'status'
end
format.atom do
......
......@@ -45,6 +45,8 @@ module Admin::ActionLogsHelper
log.recorded_changes.slice('domain', 'visible_in_picker')
elsif log.target_type == 'User' && [:promote, :demote].include?(log.action)
log.recorded_changes.slice('moderator', 'admin')
elsif log.target_type == 'User' && [:change_email].include?(log.action)
log.recorded_changes.slice('email', 'unconfirmed_email')
elsif log.target_type == 'DomainBlock'
log.recorded_changes.slice('severity', 'reject_media')
elsif log.target_type == 'Status' && log.action == :update
......@@ -84,7 +86,7 @@ module Admin::ActionLogsHelper
'positive'
when :create
opposite_verbs?(log) ? 'negative' : 'positive'
when :update, :reset_password, :disable_2fa, :memorialize
when :update, :reset_password, :disable_2fa, :memorialize, :change_email
'neutral'
when :demote, :silence, :disable, :suspend, :remove_avatar, :reopen
'negative'
......
......@@ -10,6 +10,14 @@ export function normalizeAccount(account) {
account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
account.note_emojified = emojify(account.note);
if (account.fields) {
account.fields = account.fields.map(pair => ({
...pair,
name_emojified: emojify(escapeTextContentForBrowser(pair.name)),
value_emojified: emojify(pair.value),
}));
}
if (account.moved) {
account.moved = account.moved.id;
}
......
......@@ -9,7 +9,8 @@ import {
} from './importer';
import { defineMessages } from 'react-intl';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
......@@ -39,21 +40,30 @@ const unescapeHTML = (html) => {
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
dispatch(importFetchedAccount(notification.account));
if (notification.status) {
dispatch(importFetchedStatus(notification.status));
}
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: playSound ? { sound: 'boop' } : undefined,
});
if (notification.status) {
dispatch(importFetchedStatus(notification.status));
}
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: playSound ? { sound: 'boop' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'boop' },
});
}
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert) {
......@@ -61,6 +71,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
notify.addEventListener('click', () => {
window.focus();
notify.close();
......
import api from '../api';
export const TOS_FETCH_REQUEST = 'TOS_FETCH_REQUEST';
export const TOS_FETCH_SUCCESS = 'TOS_FETCH_SUCCESS';
export const TOS_FETCH_FAIL = 'TOS_FETCH_FAIL';
export function fetchTOS() {
return (dispatch, getState) => {
dispatch(fetchTOSRequest());
api(getState).get('/static/terms-of-service.html').then(response => {
dispatch(fetchTOSSuccess(response));
}).catch(error => {
dispatch(fetchTOSFail(error));
});
};
};
export function fetchTOSRequest() {
return {
type: TOS_FETCH_REQUEST,
};
};
export function fetchTOSSuccess(data) {
return {
type: TOS_FETCH_SUCCESS,
data,
};
};
export function fetchTOSFail(error) {
return {
type: TOS_FETCH_FAIL,
error,
};
};
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
@injectIntl
export default class LoadGap extends React.PureComponent {
static propTypes = {
disabled: PropTypes.bool,
maxId: PropTypes.string,
onClick: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
handleClick = () => {
this.props.onClick(this.props.maxId);
}
render () {
const { disabled, intl } = this.props;
return (
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
<i className='fa fa-ellipsis-h' />
</button>
);
}
}
......@@ -34,7 +34,7 @@ export default class ScrollableList extends PureComponent {
};