mirror of
https://github.com/discourse/discourse.git
synced 2025-02-09 12:54:56 +00:00
DEV: Apply syntax_tree formatting to lib/*
This commit is contained in:
parent
b0fda61a8e
commit
6417173082
1
.streerc
1
.streerc
@ -1,4 +1,3 @@
|
|||||||
--print-width=100
|
--print-width=100
|
||||||
--plugins=plugin/trailing_comma,disable_ternary
|
--plugins=plugin/trailing_comma,disable_ternary
|
||||||
--ignore-files=app/*
|
--ignore-files=app/*
|
||||||
--ignore-files=lib/*
|
|
||||||
|
@ -9,9 +9,7 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::C
|
|||||||
|
|
||||||
def set_cookie(request, session_id, cookie)
|
def set_cookie(request, session_id, cookie)
|
||||||
if Hash === cookie
|
if Hash === cookie
|
||||||
if SiteSetting.force_https
|
cookie[:secure] = true if SiteSetting.force_https
|
||||||
cookie[:secure] = true
|
|
||||||
end
|
|
||||||
unless SiteSetting.same_site_cookies == "Disabled"
|
unless SiteSetting.same_site_cookies == "Disabled"
|
||||||
cookie[:same_site] = SiteSetting.same_site_cookies
|
cookie[:same_site] = SiteSetting.same_site_cookies
|
||||||
end
|
end
|
||||||
|
@ -17,10 +17,7 @@ class AdminConfirmation
|
|||||||
@token = SecureRandom.hex
|
@token = SecureRandom.hex
|
||||||
Discourse.redis.setex("admin-confirmation:#{@target_user.id}", 3.hours.to_i, @token)
|
Discourse.redis.setex("admin-confirmation:#{@target_user.id}", 3.hours.to_i, @token)
|
||||||
|
|
||||||
payload = {
|
payload = { target_user_id: @target_user.id, performed_by: @performed_by.id }
|
||||||
target_user_id: @target_user.id,
|
|
||||||
performed_by: @performed_by.id
|
|
||||||
}
|
|
||||||
Discourse.redis.setex("admin-confirmation-token:#{@token}", 3.hours.to_i, payload.to_json)
|
Discourse.redis.setex("admin-confirmation-token:#{@token}", 3.hours.to_i, payload.to_json)
|
||||||
|
|
||||||
Jobs.enqueue(
|
Jobs.enqueue(
|
||||||
@ -28,7 +25,7 @@ class AdminConfirmation
|
|||||||
to_address: @performed_by.email,
|
to_address: @performed_by.email,
|
||||||
target_email: @target_user.email,
|
target_email: @target_user.email,
|
||||||
target_username: @target_user.username,
|
target_username: @target_user.username,
|
||||||
token: @token
|
token: @token,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -51,8 +48,8 @@ class AdminConfirmation
|
|||||||
return nil unless json
|
return nil unless json
|
||||||
|
|
||||||
parsed = JSON.parse(json)
|
parsed = JSON.parse(json)
|
||||||
target_user = User.find(parsed['target_user_id'].to_i)
|
target_user = User.find(parsed["target_user_id"].to_i)
|
||||||
performed_by = User.find(parsed['performed_by'].to_i)
|
performed_by = User.find(parsed["performed_by"].to_i)
|
||||||
|
|
||||||
ac = AdminConfirmation.new(target_user, performed_by)
|
ac = AdminConfirmation.new(target_user, performed_by)
|
||||||
ac.token = token
|
ac.token = token
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AdminConstraint
|
class AdminConstraint
|
||||||
|
|
||||||
def initialize(options = {})
|
def initialize(options = {})
|
||||||
@require_master = options[:require_master]
|
@require_master = options[:require_master]
|
||||||
end
|
end
|
||||||
@ -19,5 +18,4 @@ class AdminConstraint
|
|||||||
def custom_admin_check(request)
|
def custom_admin_check(request)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AdminUserIndexQuery
|
class AdminUserIndexQuery
|
||||||
|
|
||||||
def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels)
|
def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels)
|
||||||
@params = params
|
@params = params
|
||||||
@query = initialize_query_with_order(klass)
|
@query = initialize_query_with_order(klass)
|
||||||
@ -11,24 +10,22 @@ class AdminUserIndexQuery
|
|||||||
attr_reader :params, :trust_levels
|
attr_reader :params, :trust_levels
|
||||||
|
|
||||||
SORTABLE_MAPPING = {
|
SORTABLE_MAPPING = {
|
||||||
'created' => 'created_at',
|
"created" => "created_at",
|
||||||
'last_emailed' => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))",
|
"last_emailed" => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))",
|
||||||
'seen' => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))",
|
"seen" => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))",
|
||||||
'username' => 'username',
|
"username" => "username",
|
||||||
'email' => 'email',
|
"email" => "email",
|
||||||
'trust_level' => 'trust_level',
|
"trust_level" => "trust_level",
|
||||||
'days_visited' => 'user_stats.days_visited',
|
"days_visited" => "user_stats.days_visited",
|
||||||
'posts_read' => 'user_stats.posts_read_count',
|
"posts_read" => "user_stats.posts_read_count",
|
||||||
'topics_viewed' => 'user_stats.topics_entered',
|
"topics_viewed" => "user_stats.topics_entered",
|
||||||
'posts' => 'user_stats.post_count',
|
"posts" => "user_stats.post_count",
|
||||||
'read_time' => 'user_stats.time_read'
|
"read_time" => "user_stats.time_read",
|
||||||
}
|
}
|
||||||
|
|
||||||
def find_users(limit = 100)
|
def find_users(limit = 100)
|
||||||
page = params[:page].to_i - 1
|
page = params[:page].to_i - 1
|
||||||
if page < 0
|
page = 0 if page < 0
|
||||||
page = 0
|
|
||||||
end
|
|
||||||
find_users_query.limit(limit).offset(page * limit)
|
find_users_query.limit(limit).offset(page * limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -37,7 +34,13 @@ class AdminUserIndexQuery
|
|||||||
end
|
end
|
||||||
|
|
||||||
def custom_direction
|
def custom_direction
|
||||||
Discourse.deprecate(":ascending is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') if params[:ascending]
|
if params[:ascending]
|
||||||
|
Discourse.deprecate(
|
||||||
|
":ascending is deprecated please use :asc instead",
|
||||||
|
output_in_test: true,
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
|
end
|
||||||
asc = params[:asc] || params[:ascending]
|
asc = params[:asc] || params[:ascending]
|
||||||
asc.present? && asc ? "ASC" : "DESC"
|
asc.present? && asc ? "ASC" : "DESC"
|
||||||
end
|
end
|
||||||
@ -47,7 +50,7 @@ class AdminUserIndexQuery
|
|||||||
|
|
||||||
custom_order = params[:order]
|
custom_order = params[:order]
|
||||||
if custom_order.present? &&
|
if custom_order.present? &&
|
||||||
without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, '')]
|
without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, "")]
|
||||||
order << "#{without_dir} #{custom_direction}"
|
order << "#{without_dir} #{custom_direction}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -61,13 +64,9 @@ class AdminUserIndexQuery
|
|||||||
order << "users.username"
|
order << "users.username"
|
||||||
end
|
end
|
||||||
|
|
||||||
query = klass
|
query = klass.includes(:totps).order(order.reject(&:blank?).join(","))
|
||||||
.includes(:totps)
|
|
||||||
.order(order.reject(&:blank?).join(","))
|
|
||||||
|
|
||||||
unless params[:stats].present? && params[:stats] == false
|
query = query.includes(:user_stat) unless params[:stats].present? && params[:stats] == false
|
||||||
query = query.includes(:user_stat)
|
|
||||||
end
|
|
||||||
|
|
||||||
query = query.joins(:primary_email) if params[:show_emails] == "true"
|
query = query.joins(:primary_email) if params[:show_emails] == "true"
|
||||||
|
|
||||||
@ -77,32 +76,44 @@ class AdminUserIndexQuery
|
|||||||
def filter_by_trust
|
def filter_by_trust
|
||||||
levels = trust_levels.map { |key, _| key.to_s }
|
levels = trust_levels.map { |key, _| key.to_s }
|
||||||
if levels.include?(params[:query])
|
if levels.include?(params[:query])
|
||||||
@query.where('trust_level = ?', trust_levels[params[:query].to_sym])
|
@query.where("trust_level = ?", trust_levels[params[:query].to_sym])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_query_classification
|
def filter_by_query_classification
|
||||||
case params[:query]
|
case params[:query]
|
||||||
when 'staff' then @query.where("admin or moderator")
|
when "staff"
|
||||||
when 'admins' then @query.where(admin: true)
|
@query.where("admin or moderator")
|
||||||
when 'moderators' then @query.where(moderator: true)
|
when "admins"
|
||||||
when 'silenced' then @query.silenced
|
@query.where(admin: true)
|
||||||
when 'suspended' then @query.suspended
|
when "moderators"
|
||||||
when 'pending' then @query.not_suspended.where(approved: false, active: true)
|
@query.where(moderator: true)
|
||||||
when 'staged' then @query.where(staged: true)
|
when "silenced"
|
||||||
|
@query.silenced
|
||||||
|
when "suspended"
|
||||||
|
@query.suspended
|
||||||
|
when "pending"
|
||||||
|
@query.not_suspended.where(approved: false, active: true)
|
||||||
|
when "staged"
|
||||||
|
@query.where(staged: true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_search
|
def filter_by_search
|
||||||
if params[:email].present?
|
if params[:email].present?
|
||||||
return @query.joins(:primary_email).where('user_emails.email = ?', params[:email].downcase)
|
return @query.joins(:primary_email).where("user_emails.email = ?", params[:email].downcase)
|
||||||
end
|
end
|
||||||
|
|
||||||
filter = params[:filter]
|
filter = params[:filter]
|
||||||
if filter.present?
|
if filter.present?
|
||||||
filter = filter.strip
|
filter = filter.strip
|
||||||
if ip = IPAddr.new(filter) rescue nil
|
if ip =
|
||||||
@query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s)
|
begin
|
||||||
|
IPAddr.new(filter)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
@query.where("ip_address <<= :ip OR registration_ip_address <<= :ip", ip: ip.to_cidr_s)
|
||||||
else
|
else
|
||||||
@query.filter_by_username_or_email(filter)
|
@query.filter_by_username_or_email(filter)
|
||||||
end
|
end
|
||||||
@ -111,14 +122,12 @@ class AdminUserIndexQuery
|
|||||||
|
|
||||||
def filter_by_ip
|
def filter_by_ip
|
||||||
if params[:ip].present?
|
if params[:ip].present?
|
||||||
@query.where('ip_address = :ip OR registration_ip_address = :ip', ip: params[:ip].strip)
|
@query.where("ip_address = :ip OR registration_ip_address = :ip", ip: params[:ip].strip)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_exclude
|
def filter_exclude
|
||||||
if params[:exclude].present?
|
@query.where("users.id != ?", params[:exclude]) if params[:exclude].present?
|
||||||
@query.where('users.id != ?', params[:exclude])
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# this might not be needed in rails 4 ?
|
# this might not be needed in rails 4 ?
|
||||||
@ -134,5 +143,4 @@ class AdminUserIndexQuery
|
|||||||
append filter_by_search
|
append filter_by_search
|
||||||
@query
|
@query
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module AgeWords
|
module AgeWords
|
||||||
|
|
||||||
def self.age_words(secs)
|
def self.age_words(secs)
|
||||||
if secs.blank?
|
if secs.blank?
|
||||||
"—"
|
"—"
|
||||||
@ -10,5 +9,4 @@ module AgeWords
|
|||||||
FreedomPatches::Rails4.distance_of_time_in_words(now, now + secs)
|
FreedomPatches::Rails4.distance_of_time_in_words(now, now + secs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -11,22 +11,19 @@ class Archetype
|
|||||||
end
|
end
|
||||||
|
|
||||||
def attributes
|
def attributes
|
||||||
{
|
{ id: @id, options: @options }
|
||||||
id: @id,
|
|
||||||
options: @options
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.default
|
def self.default
|
||||||
'regular'
|
"regular"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.private_message
|
def self.private_message
|
||||||
'private_message'
|
"private_message"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.banner
|
def self.banner
|
||||||
'banner'
|
"banner"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.list
|
def self.list
|
||||||
@ -40,8 +37,7 @@ class Archetype
|
|||||||
end
|
end
|
||||||
|
|
||||||
# default archetypes
|
# default archetypes
|
||||||
register 'regular'
|
register "regular"
|
||||||
register 'private_message'
|
register "private_message"
|
||||||
register 'banner'
|
register "banner"
|
||||||
|
|
||||||
end
|
end
|
||||||
|
21
lib/auth.rb
21
lib/auth.rb
@ -1,13 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Auth; end
|
module Auth
|
||||||
|
end
|
||||||
|
|
||||||
require 'auth/auth_provider'
|
require "auth/auth_provider"
|
||||||
require 'auth/result'
|
require "auth/result"
|
||||||
require 'auth/authenticator'
|
require "auth/authenticator"
|
||||||
require 'auth/managed_authenticator'
|
require "auth/managed_authenticator"
|
||||||
require 'auth/facebook_authenticator'
|
require "auth/facebook_authenticator"
|
||||||
require 'auth/github_authenticator'
|
require "auth/github_authenticator"
|
||||||
require 'auth/twitter_authenticator'
|
require "auth/twitter_authenticator"
|
||||||
require 'auth/google_oauth2_authenticator'
|
require "auth/google_oauth2_authenticator"
|
||||||
require 'auth/discord_authenticator'
|
require "auth/discord_authenticator"
|
||||||
|
@ -8,32 +8,60 @@ class Auth::AuthProvider
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.auth_attributes
|
def self.auth_attributes
|
||||||
[:authenticator, :pretty_name, :title, :message, :frame_width, :frame_height,
|
%i[
|
||||||
:pretty_name_setting, :title_setting, :enabled_setting, :full_screen_login, :full_screen_login_setting,
|
authenticator
|
||||||
:custom_url, :background_color, :icon]
|
pretty_name
|
||||||
|
title
|
||||||
|
message
|
||||||
|
frame_width
|
||||||
|
frame_height
|
||||||
|
pretty_name_setting
|
||||||
|
title_setting
|
||||||
|
enabled_setting
|
||||||
|
full_screen_login
|
||||||
|
full_screen_login_setting
|
||||||
|
custom_url
|
||||||
|
background_color
|
||||||
|
icon
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_accessor(*auth_attributes)
|
attr_accessor(*auth_attributes)
|
||||||
|
|
||||||
def enabled_setting=(val)
|
def enabled_setting=(val)
|
||||||
Discourse.deprecate("(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
|
"(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead",
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
@enabled_setting = val
|
@enabled_setting = val
|
||||||
end
|
end
|
||||||
|
|
||||||
def background_color=(val)
|
def background_color=(val)
|
||||||
Discourse.deprecate("(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
|
"(#{authenticator.name}) background_color is no longer functional. Please use CSS instead",
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_screen_login=(val)
|
def full_screen_login=(val)
|
||||||
Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
|
"(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.",
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def full_screen_login_setting=(val)
|
def full_screen_login_setting=(val)
|
||||||
Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
|
"(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.",
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def message=(val)
|
def message=(val)
|
||||||
Discourse.deprecate("(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
|
"(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider",
|
||||||
|
drop_from: "2.9.0",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
@ -47,5 +75,4 @@ class Auth::AuthProvider
|
|||||||
def can_revoke
|
def can_revoke
|
||||||
authenticator.can_revoke?
|
authenticator.can_revoke?
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Auth; end
|
module Auth
|
||||||
|
end
|
||||||
class Auth::CurrentUserProvider
|
class Auth::CurrentUserProvider
|
||||||
|
|
||||||
# do all current user initialization here
|
# do all current user initialization here
|
||||||
def initialize(env)
|
def initialize(env)
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require_relative '../route_matcher'
|
require_relative "../route_matcher"
|
||||||
|
|
||||||
# You may have seen references to v0 and v1 of our auth cookie in the codebase
|
# You may have seen references to v0 and v1 of our auth cookie in the codebase
|
||||||
# and you're not sure how they differ, so here is an explanation:
|
# and you're not sure how they differ, so here is an explanation:
|
||||||
@ -23,7 +23,6 @@ require_relative '../route_matcher'
|
|||||||
# We'll drop support for v0 after Discourse 2.9 is released.
|
# We'll drop support for v0 after Discourse 2.9 is released.
|
||||||
|
|
||||||
class Auth::DefaultCurrentUserProvider
|
class Auth::DefaultCurrentUserProvider
|
||||||
|
|
||||||
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
|
CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER"
|
||||||
USER_TOKEN_KEY ||= "_DISCOURSE_USER_TOKEN"
|
USER_TOKEN_KEY ||= "_DISCOURSE_USER_TOKEN"
|
||||||
API_KEY ||= "api_key"
|
API_KEY ||= "api_key"
|
||||||
@ -37,7 +36,7 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID"
|
USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID"
|
||||||
API_KEY_ENV ||= "_DISCOURSE_API"
|
API_KEY_ENV ||= "_DISCOURSE_API"
|
||||||
USER_API_KEY_ENV ||= "_DISCOURSE_USER_API"
|
USER_API_KEY_ENV ||= "_DISCOURSE_USER_API"
|
||||||
TOKEN_COOKIE ||= ENV['DISCOURSE_TOKEN_COOKIE'] || "_t"
|
TOKEN_COOKIE ||= ENV["DISCOURSE_TOKEN_COOKIE"] || "_t"
|
||||||
PATH_INFO ||= "PATH_INFO"
|
PATH_INFO ||= "PATH_INFO"
|
||||||
COOKIE_ATTEMPTS_PER_MIN ||= 10
|
COOKIE_ATTEMPTS_PER_MIN ||= 10
|
||||||
BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN"
|
BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN"
|
||||||
@ -59,30 +58,20 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
"badges#show",
|
"badges#show",
|
||||||
"tags#tag_feed",
|
"tags#tag_feed",
|
||||||
"tags#show",
|
"tags#show",
|
||||||
*[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "list##{f}_feed" },
|
*%i[latest unread new read posted bookmarks].map { |f| "list##{f}_feed" },
|
||||||
*[:all, :yearly, :quarterly, :monthly, :weekly, :daily].map { |p| "list#top_#{p}_feed" },
|
*%i[all yearly quarterly monthly weekly daily].map { |p| "list#top_#{p}_feed" },
|
||||||
*[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "tags#show_#{f}" }
|
*%i[latest unread new read posted bookmarks].map { |f| "tags#show_#{f}" },
|
||||||
],
|
],
|
||||||
formats: :rss
|
formats: :rss,
|
||||||
),
|
|
||||||
RouteMatcher.new(
|
|
||||||
methods: :get,
|
|
||||||
actions: "users#bookmarks",
|
|
||||||
formats: :ics
|
|
||||||
),
|
|
||||||
RouteMatcher.new(
|
|
||||||
methods: :post,
|
|
||||||
actions: "admin/email#handle_mail",
|
|
||||||
formats: nil
|
|
||||||
),
|
),
|
||||||
|
RouteMatcher.new(methods: :get, actions: "users#bookmarks", formats: :ics),
|
||||||
|
RouteMatcher.new(methods: :post, actions: "admin/email#handle_mail", formats: nil),
|
||||||
]
|
]
|
||||||
|
|
||||||
def self.find_v0_auth_cookie(request)
|
def self.find_v0_auth_cookie(request)
|
||||||
cookie = request.cookies[TOKEN_COOKIE]
|
cookie = request.cookies[TOKEN_COOKIE]
|
||||||
|
|
||||||
if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE
|
cookie if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE
|
||||||
cookie
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_v1_auth_cookie(env)
|
def self.find_v1_auth_cookie(env)
|
||||||
@ -111,12 +100,10 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY)
|
return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY)
|
||||||
|
|
||||||
# bypass if we have the shared session header
|
# bypass if we have the shared session header
|
||||||
if shared_key = @env['HTTP_X_SHARED_SESSION_KEY']
|
if shared_key = @env["HTTP_X_SHARED_SESSION_KEY"]
|
||||||
uid = Discourse.redis.get("shared_session_key_#{shared_key}")
|
uid = Discourse.redis.get("shared_session_key_#{shared_key}")
|
||||||
user = nil
|
user = nil
|
||||||
if uid
|
user = User.find_by(id: uid.to_i) if uid
|
||||||
user = User.find_by(id: uid.to_i)
|
|
||||||
end
|
|
||||||
@env[CURRENT_USER_KEY] = user
|
@env[CURRENT_USER_KEY] = user
|
||||||
return user
|
return user
|
||||||
end
|
end
|
||||||
@ -130,28 +117,27 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
user_api_key ||= request[PARAMETER_USER_API_KEY]
|
user_api_key ||= request[PARAMETER_USER_API_KEY]
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@env.blank? && request[API_KEY] && api_parameter_allowed?
|
api_key ||= request[API_KEY] if !@env.blank? && request[API_KEY] && api_parameter_allowed?
|
||||||
api_key ||= request[API_KEY]
|
|
||||||
end
|
|
||||||
|
|
||||||
auth_token = find_auth_token
|
auth_token = find_auth_token
|
||||||
current_user = nil
|
current_user = nil
|
||||||
|
|
||||||
if auth_token
|
if auth_token
|
||||||
limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60)
|
limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN, 60)
|
||||||
|
|
||||||
if limiter.can_perform?
|
if limiter.can_perform?
|
||||||
@env[USER_TOKEN_KEY] = @user_token = begin
|
@env[USER_TOKEN_KEY] = @user_token =
|
||||||
UserAuthToken.lookup(
|
begin
|
||||||
auth_token,
|
UserAuthToken.lookup(
|
||||||
seen: true,
|
auth_token,
|
||||||
user_agent: @env['HTTP_USER_AGENT'],
|
seen: true,
|
||||||
path: @env['REQUEST_PATH'],
|
user_agent: @env["HTTP_USER_AGENT"],
|
||||||
client_ip: @request.ip
|
path: @env["REQUEST_PATH"],
|
||||||
)
|
client_ip: @request.ip,
|
||||||
rescue ActiveRecord::ReadOnlyError
|
)
|
||||||
nil
|
rescue ActiveRecord::ReadOnlyError
|
||||||
end
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
current_user = @user_token.try(:user)
|
current_user = @user_token.try(:user)
|
||||||
end
|
end
|
||||||
@ -161,14 +147,10 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
begin
|
begin
|
||||||
limiter.performed!
|
limiter.performed!
|
||||||
rescue RateLimiter::LimitExceeded
|
rescue RateLimiter::LimitExceeded
|
||||||
raise Discourse::InvalidAccess.new(
|
raise Discourse::InvalidAccess.new("Invalid Access", nil, delete_cookie: TOKEN_COOKIE)
|
||||||
'Invalid Access',
|
|
||||||
nil,
|
|
||||||
delete_cookie: TOKEN_COOKIE
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
elsif @env['HTTP_DISCOURSE_LOGGED_IN']
|
elsif @env["HTTP_DISCOURSE_LOGGED_IN"]
|
||||||
@env[BAD_TOKEN] = true
|
@env[BAD_TOKEN] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -177,10 +159,10 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
current_user = lookup_api_user(api_key, request)
|
current_user = lookup_api_user(api_key, request)
|
||||||
if !current_user
|
if !current_user
|
||||||
raise Discourse::InvalidAccess.new(
|
raise Discourse::InvalidAccess.new(
|
||||||
I18n.t('invalid_api_credentials'),
|
I18n.t("invalid_api_credentials"),
|
||||||
nil,
|
nil,
|
||||||
custom_message: "invalid_api_credentials"
|
custom_message: "invalid_api_credentials",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
||||||
admin_api_key_limiter.performed! if !Rails.env.profile?
|
admin_api_key_limiter.performed! if !Rails.env.profile?
|
||||||
@ -191,12 +173,13 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
if user_api_key
|
if user_api_key
|
||||||
@hashed_user_api_key = ApiKey.hash_key(user_api_key)
|
@hashed_user_api_key = ApiKey.hash_key(user_api_key)
|
||||||
|
|
||||||
user_api_key_obj = UserApiKey
|
user_api_key_obj =
|
||||||
.active
|
UserApiKey
|
||||||
.joins(:user)
|
.active
|
||||||
.where(key_hash: @hashed_user_api_key)
|
.joins(:user)
|
||||||
.includes(:user, :scopes)
|
.where(key_hash: @hashed_user_api_key)
|
||||||
.first
|
.includes(:user, :scopes)
|
||||||
|
.first
|
||||||
|
|
||||||
raise Discourse::InvalidAccess unless user_api_key_obj
|
raise Discourse::InvalidAccess unless user_api_key_obj
|
||||||
|
|
||||||
@ -208,18 +191,14 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
current_user = user_api_key_obj.user
|
current_user = user_api_key_obj.user
|
||||||
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active
|
||||||
|
|
||||||
if can_write?
|
user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) if can_write?
|
||||||
user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID])
|
|
||||||
end
|
|
||||||
|
|
||||||
@env[USER_API_KEY_ENV] = true
|
@env[USER_API_KEY_ENV] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
# keep this rule here as a safeguard
|
# keep this rule here as a safeguard
|
||||||
# under no conditions to suspended or inactive accounts get current_user
|
# under no conditions to suspended or inactive accounts get current_user
|
||||||
if current_user && (current_user.suspended? || !current_user.active)
|
current_user = nil if current_user && (current_user.suspended? || !current_user.active)
|
||||||
current_user = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
if current_user && should_update_last_seen?
|
if current_user && should_update_last_seen?
|
||||||
ip = request.ip
|
ip = request.ip
|
||||||
@ -247,31 +226,40 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
if !is_user_api? && @user_token && @user_token.user == user
|
if !is_user_api? && @user_token && @user_token.user == user
|
||||||
rotated_at = @user_token.rotated_at
|
rotated_at = @user_token.rotated_at
|
||||||
|
|
||||||
needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago
|
needs_rotation =
|
||||||
|
(
|
||||||
|
if @user_token.auth_token_seen
|
||||||
|
rotated_at < UserAuthToken::ROTATE_TIME.ago
|
||||||
|
else
|
||||||
|
rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
if needs_rotation
|
if needs_rotation
|
||||||
if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'],
|
if @user_token.rotate!(
|
||||||
client_ip: @request.ip,
|
user_agent: @env["HTTP_USER_AGENT"],
|
||||||
path: @env['REQUEST_PATH'])
|
client_ip: @request.ip,
|
||||||
|
path: @env["REQUEST_PATH"],
|
||||||
|
)
|
||||||
set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar)
|
set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar)
|
||||||
DiscourseEvent.trigger(:user_session_refreshed, user)
|
DiscourseEvent.trigger(:user_session_refreshed, user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if !user && cookie_jar.key?(TOKEN_COOKIE)
|
cookie_jar.delete(TOKEN_COOKIE) if !user && cookie_jar.key?(TOKEN_COOKIE)
|
||||||
cookie_jar.delete(TOKEN_COOKIE)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def log_on_user(user, session, cookie_jar, opts = {})
|
def log_on_user(user, session, cookie_jar, opts = {})
|
||||||
@env[USER_TOKEN_KEY] = @user_token = UserAuthToken.generate!(
|
@env[USER_TOKEN_KEY] = @user_token =
|
||||||
user_id: user.id,
|
UserAuthToken.generate!(
|
||||||
user_agent: @env['HTTP_USER_AGENT'],
|
user_id: user.id,
|
||||||
path: @env['REQUEST_PATH'],
|
user_agent: @env["HTTP_USER_AGENT"],
|
||||||
client_ip: @request.ip,
|
path: @env["REQUEST_PATH"],
|
||||||
staff: user.staff?,
|
client_ip: @request.ip,
|
||||||
impersonate: opts[:impersonate])
|
staff: user.staff?,
|
||||||
|
impersonate: opts[:impersonate],
|
||||||
|
)
|
||||||
|
|
||||||
set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar)
|
set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar)
|
||||||
user.unstage!
|
user.unstage!
|
||||||
@ -288,23 +276,19 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
token: unhashed_auth_token,
|
token: unhashed_auth_token,
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
trust_level: user.trust_level,
|
trust_level: user.trust_level,
|
||||||
issued_at: Time.zone.now.to_i
|
issued_at: Time.zone.now.to_i,
|
||||||
}
|
}
|
||||||
|
|
||||||
if SiteSetting.persistent_sessions
|
expires = SiteSetting.maximum_session_age.hours.from_now if SiteSetting.persistent_sessions
|
||||||
expires = SiteSetting.maximum_session_age.hours.from_now
|
|
||||||
end
|
|
||||||
|
|
||||||
if SiteSetting.same_site_cookies != "Disabled"
|
same_site = SiteSetting.same_site_cookies if SiteSetting.same_site_cookies != "Disabled"
|
||||||
same_site = SiteSetting.same_site_cookies
|
|
||||||
end
|
|
||||||
|
|
||||||
cookie_jar.encrypted[TOKEN_COOKIE] = {
|
cookie_jar.encrypted[TOKEN_COOKIE] = {
|
||||||
value: data,
|
value: data,
|
||||||
httponly: true,
|
httponly: true,
|
||||||
secure: SiteSetting.force_https,
|
secure: SiteSetting.force_https,
|
||||||
expires: expires,
|
expires: expires,
|
||||||
same_site: same_site
|
same_site: same_site,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -313,10 +297,8 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
# for signup flow, since all admin emails are stored in
|
# for signup flow, since all admin emails are stored in
|
||||||
# DISCOURSE_DEVELOPER_EMAILS for self-hosters.
|
# DISCOURSE_DEVELOPER_EMAILS for self-hosters.
|
||||||
def make_developer_admin(user)
|
def make_developer_admin(user)
|
||||||
if user.active? &&
|
if user.active? && !user.admin && Rails.configuration.respond_to?(:developer_emails) &&
|
||||||
!user.admin &&
|
Rails.configuration.developer_emails.include?(user.email)
|
||||||
Rails.configuration.respond_to?(:developer_emails) &&
|
|
||||||
Rails.configuration.developer_emails.include?(user.email)
|
|
||||||
user.admin = true
|
user.admin = true
|
||||||
user.save
|
user.save
|
||||||
Group.refresh_automatic_groups!(:staff, :admins)
|
Group.refresh_automatic_groups!(:staff, :admins)
|
||||||
@ -347,7 +329,7 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
@user_token.destroy
|
@user_token.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
cookie_jar.delete('authentication_data')
|
cookie_jar.delete("authentication_data")
|
||||||
cookie_jar.delete(TOKEN_COOKIE)
|
cookie_jar.delete(TOKEN_COOKIE)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -384,9 +366,7 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first
|
if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first
|
||||||
api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME]
|
api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME]
|
||||||
|
|
||||||
if !api_key.request_allowed?(@env)
|
return nil if !api_key.request_allowed?(@env)
|
||||||
return nil
|
|
||||||
end
|
|
||||||
|
|
||||||
user =
|
user =
|
||||||
if api_key.user
|
if api_key.user
|
||||||
@ -395,7 +375,8 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
User.find_by(username_lower: api_username.downcase)
|
User.find_by(username_lower: api_username.downcase)
|
||||||
elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"]
|
elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"]
|
||||||
User.find_by(id: user_id.to_i)
|
User.find_by(id: user_id.to_i)
|
||||||
elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"]
|
elsif external_id =
|
||||||
|
header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"]
|
||||||
SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user)
|
SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -435,52 +416,48 @@ class Auth::DefaultCurrentUserProvider
|
|||||||
|
|
||||||
limit = GlobalSetting.max_admin_api_reqs_per_minute.to_i
|
limit = GlobalSetting.max_admin_api_reqs_per_minute.to_i
|
||||||
if GlobalSetting.respond_to?(:max_admin_api_reqs_per_key_per_minute)
|
if GlobalSetting.respond_to?(:max_admin_api_reqs_per_key_per_minute)
|
||||||
Discourse.deprecate("DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", drop_from: '2.9.0')
|
Discourse.deprecate(
|
||||||
limit = [
|
"DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE",
|
||||||
GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i,
|
drop_from: "2.9.0",
|
||||||
limit
|
)
|
||||||
].max
|
limit = [GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, limit].max
|
||||||
end
|
end
|
||||||
@admin_api_key_limiter = RateLimiter.new(
|
@admin_api_key_limiter =
|
||||||
nil,
|
RateLimiter.new(nil, "admin_api_min", limit, 60, error_code: "admin_api_key_rate_limit")
|
||||||
"admin_api_min",
|
|
||||||
limit,
|
|
||||||
60,
|
|
||||||
error_code: "admin_api_key_rate_limit"
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_api_key_limiter_60_secs
|
def user_api_key_limiter_60_secs
|
||||||
@user_api_key_limiter_60_secs ||= RateLimiter.new(
|
@user_api_key_limiter_60_secs ||=
|
||||||
nil,
|
RateLimiter.new(
|
||||||
"user_api_min_#{@hashed_user_api_key}",
|
nil,
|
||||||
GlobalSetting.max_user_api_reqs_per_minute,
|
"user_api_min_#{@hashed_user_api_key}",
|
||||||
60,
|
GlobalSetting.max_user_api_reqs_per_minute,
|
||||||
error_code: "user_api_key_limiter_60_secs"
|
60,
|
||||||
)
|
error_code: "user_api_key_limiter_60_secs",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def user_api_key_limiter_1_day
|
def user_api_key_limiter_1_day
|
||||||
@user_api_key_limiter_1_day ||= RateLimiter.new(
|
@user_api_key_limiter_1_day ||=
|
||||||
nil,
|
RateLimiter.new(
|
||||||
"user_api_day_#{@hashed_user_api_key}",
|
nil,
|
||||||
GlobalSetting.max_user_api_reqs_per_day,
|
"user_api_day_#{@hashed_user_api_key}",
|
||||||
86400,
|
GlobalSetting.max_user_api_reqs_per_day,
|
||||||
error_code: "user_api_key_limiter_1_day"
|
86_400,
|
||||||
)
|
error_code: "user_api_key_limiter_1_day",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_auth_token
|
def find_auth_token
|
||||||
return @auth_token if defined?(@auth_token)
|
return @auth_token if defined?(@auth_token)
|
||||||
|
|
||||||
@auth_token = begin
|
@auth_token =
|
||||||
if v0 = self.class.find_v0_auth_cookie(@request)
|
begin
|
||||||
v0
|
if v0 = self.class.find_v0_auth_cookie(@request)
|
||||||
elsif v1 = self.class.find_v1_auth_cookie(@env)
|
v0
|
||||||
if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i
|
elsif v1 = self.class.find_v1_auth_cookie(@env)
|
||||||
v1[:token]
|
v1[:token] if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,35 +2,34 @@
|
|||||||
|
|
||||||
class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator
|
class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator
|
||||||
class DiscordStrategy < OmniAuth::Strategies::OAuth2
|
class DiscordStrategy < OmniAuth::Strategies::OAuth2
|
||||||
option :name, 'discord'
|
option :name, "discord"
|
||||||
option :scope, 'identify email guilds'
|
option :scope, "identify email guilds"
|
||||||
|
|
||||||
option :client_options,
|
option :client_options,
|
||||||
site: 'https://discord.com/api',
|
site: "https://discord.com/api",
|
||||||
authorize_url: 'oauth2/authorize',
|
authorize_url: "oauth2/authorize",
|
||||||
token_url: 'oauth2/token'
|
token_url: "oauth2/token"
|
||||||
|
|
||||||
option :authorize_options, %i[scope permissions]
|
option :authorize_options, %i[scope permissions]
|
||||||
|
|
||||||
uid { raw_info['id'] }
|
uid { raw_info["id"] }
|
||||||
|
|
||||||
info do
|
info do
|
||||||
{
|
{
|
||||||
name: raw_info['username'],
|
name: raw_info["username"],
|
||||||
email: raw_info['verified'] ? raw_info['email'] : nil,
|
email: raw_info["verified"] ? raw_info["email"] : nil,
|
||||||
image: "https://cdn.discordapp.com/avatars/#{raw_info['id']}/#{raw_info['avatar']}"
|
image: "https://cdn.discordapp.com/avatars/#{raw_info["id"]}/#{raw_info["avatar"]}",
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
extra do
|
extra { { "raw_info" => raw_info } }
|
||||||
{
|
|
||||||
'raw_info' => raw_info
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw_info
|
def raw_info
|
||||||
@raw_info ||= access_token.get('users/@me').parsed.
|
@raw_info ||=
|
||||||
merge(guilds: access_token.get('users/@me/guilds').parsed)
|
access_token
|
||||||
|
.get("users/@me")
|
||||||
|
.parsed
|
||||||
|
.merge(guilds: access_token.get("users/@me/guilds").parsed)
|
||||||
end
|
end
|
||||||
|
|
||||||
def callback_url
|
def callback_url
|
||||||
@ -39,7 +38,7 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator
|
|||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
'discord'
|
"discord"
|
||||||
end
|
end
|
||||||
|
|
||||||
def enabled?
|
def enabled?
|
||||||
@ -48,23 +47,26 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator
|
|||||||
|
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
omniauth.provider DiscordStrategy,
|
omniauth.provider DiscordStrategy,
|
||||||
setup: lambda { |env|
|
setup:
|
||||||
strategy = env["omniauth.strategy"]
|
lambda { |env|
|
||||||
strategy.options[:client_id] = SiteSetting.discord_client_id
|
strategy = env["omniauth.strategy"]
|
||||||
strategy.options[:client_secret] = SiteSetting.discord_secret
|
strategy.options[:client_id] = SiteSetting.discord_client_id
|
||||||
}
|
strategy.options[:client_secret] = SiteSetting.discord_secret
|
||||||
end
|
}
|
||||||
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_token, existing_account: nil)
|
def after_authenticate(auth_token, existing_account: nil)
|
||||||
allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|")
|
allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|")
|
||||||
|
|
||||||
if allowed_guild_ids.length > 0
|
if allowed_guild_ids.length > 0
|
||||||
user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g['id'] }
|
user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g["id"] }
|
||||||
if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds
|
if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds
|
||||||
return Auth::Result.new.tap do |auth_result|
|
return(
|
||||||
auth_result.failed = true
|
Auth::Result.new.tap do |auth_result|
|
||||||
auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild")
|
auth_result.failed = true
|
||||||
end
|
auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild")
|
||||||
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator
|
class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator
|
||||||
|
|
||||||
AVATAR_SIZE ||= 480
|
AVATAR_SIZE ||= 480
|
||||||
|
|
||||||
def name
|
def name
|
||||||
@ -14,15 +13,19 @@ class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator
|
|||||||
|
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
omniauth.provider :facebook,
|
omniauth.provider :facebook,
|
||||||
setup: lambda { |env|
|
setup:
|
||||||
strategy = env["omniauth.strategy"]
|
lambda { |env|
|
||||||
strategy.options[:client_id] = SiteSetting.facebook_app_id
|
strategy = env["omniauth.strategy"]
|
||||||
strategy.options[:client_secret] = SiteSetting.facebook_app_secret
|
strategy.options[:client_id] = SiteSetting.facebook_app_id
|
||||||
strategy.options[:info_fields] = 'name,first_name,last_name,email'
|
strategy.options[:client_secret] = SiteSetting.facebook_app_secret
|
||||||
strategy.options[:image_size] = { width: AVATAR_SIZE, height: AVATAR_SIZE }
|
strategy.options[:info_fields] = "name,first_name,last_name,email"
|
||||||
strategy.options[:secure_image_url] = true
|
strategy.options[:image_size] = {
|
||||||
},
|
width: AVATAR_SIZE,
|
||||||
scope: "email"
|
height: AVATAR_SIZE,
|
||||||
|
}
|
||||||
|
strategy.options[:secure_image_url] = true
|
||||||
|
},
|
||||||
|
scope: "email"
|
||||||
end
|
end
|
||||||
|
|
||||||
# facebook doesn't return unverified email addresses so it's safe to assume
|
# facebook doesn't return unverified email addresses so it's safe to assume
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'has_errors'
|
require "has_errors"
|
||||||
|
|
||||||
class Auth::GithubAuthenticator < Auth::ManagedAuthenticator
|
class Auth::GithubAuthenticator < Auth::ManagedAuthenticator
|
||||||
|
|
||||||
def name
|
def name
|
||||||
"github"
|
"github"
|
||||||
end
|
end
|
||||||
@ -50,12 +49,13 @@ class Auth::GithubAuthenticator < Auth::ManagedAuthenticator
|
|||||||
|
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
omniauth.provider :github,
|
omniauth.provider :github,
|
||||||
setup: lambda { |env|
|
setup:
|
||||||
strategy = env["omniauth.strategy"]
|
lambda { |env|
|
||||||
strategy.options[:client_id] = SiteSetting.github_client_id
|
strategy = env["omniauth.strategy"]
|
||||||
strategy.options[:client_secret] = SiteSetting.github_client_secret
|
strategy.options[:client_id] = SiteSetting.github_client_id
|
||||||
},
|
strategy.options[:client_secret] = SiteSetting.github_client_secret
|
||||||
scope: "user:email"
|
},
|
||||||
|
scope: "user:email"
|
||||||
end
|
end
|
||||||
|
|
||||||
# the omniauth-github gem only picks up the primary email if it's verified:
|
# the omniauth-github gem only picks up the primary email if it's verified:
|
||||||
|
@ -22,47 +22,46 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator
|
|||||||
|
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
options = {
|
options = {
|
||||||
setup: lambda { |env|
|
setup:
|
||||||
strategy = env["omniauth.strategy"]
|
lambda do |env|
|
||||||
strategy.options[:client_id] = SiteSetting.google_oauth2_client_id
|
strategy = env["omniauth.strategy"]
|
||||||
strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret
|
strategy.options[:client_id] = SiteSetting.google_oauth2_client_id
|
||||||
|
strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret
|
||||||
|
|
||||||
if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present?
|
if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present?
|
||||||
strategy.options[:hd] = google_oauth2_hd
|
strategy.options[:hd] = google_oauth2_hd
|
||||||
end
|
end
|
||||||
|
|
||||||
if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present?
|
if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present?
|
||||||
strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ")
|
strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ")
|
||||||
end
|
end
|
||||||
|
|
||||||
# All the data we need for the `info` and `credentials` auth hash
|
# All the data we need for the `info` and `credentials` auth hash
|
||||||
# are obtained via the user info API, not the JWT. Using and verifying
|
# are obtained via the user info API, not the JWT. Using and verifying
|
||||||
# the JWT can fail due to clock skew, so let's skip it completely.
|
# the JWT can fail due to clock skew, so let's skip it completely.
|
||||||
# https://github.com/zquestz/omniauth-google-oauth2/pull/392
|
# https://github.com/zquestz/omniauth-google-oauth2/pull/392
|
||||||
strategy.options[:skip_jwt] = true
|
strategy.options[:skip_jwt] = true
|
||||||
}
|
end,
|
||||||
}
|
}
|
||||||
omniauth.provider :google_oauth2, options
|
omniauth.provider :google_oauth2, options
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_authenticate(auth_token, existing_account: nil)
|
def after_authenticate(auth_token, existing_account: nil)
|
||||||
groups = provides_groups? ? raw_groups(auth_token.uid) : nil
|
groups = provides_groups? ? raw_groups(auth_token.uid) : nil
|
||||||
if groups
|
auth_token.extra[:raw_groups] = groups if groups
|
||||||
auth_token.extra[:raw_groups] = groups
|
|
||||||
end
|
|
||||||
|
|
||||||
result = super
|
result = super
|
||||||
|
|
||||||
if groups
|
if groups
|
||||||
result.associated_groups = groups.map { |group| group.with_indifferent_access.slice(:id, :name) }
|
result.associated_groups =
|
||||||
|
groups.map { |group| group.with_indifferent_access.slice(:id, :name) }
|
||||||
end
|
end
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def provides_groups?
|
def provides_groups?
|
||||||
SiteSetting.google_oauth2_hd.present? &&
|
SiteSetting.google_oauth2_hd.present? && SiteSetting.google_oauth2_hd_groups &&
|
||||||
SiteSetting.google_oauth2_hd_groups &&
|
|
||||||
SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? &&
|
SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? &&
|
||||||
SiteSetting.google_oauth2_hd_groups_service_account_json.present?
|
SiteSetting.google_oauth2_hd_groups_service_account_json.present?
|
||||||
end
|
end
|
||||||
@ -77,20 +76,20 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator
|
|||||||
return if client.nil?
|
return if client.nil?
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
params = {
|
params = { userKey: uid }
|
||||||
userKey: uid
|
|
||||||
}
|
|
||||||
params[:pageToken] = page_token if page_token
|
params[:pageToken] = page_token if page_token
|
||||||
|
|
||||||
response = client.get(groups_url, params: params, raise_errors: false)
|
response = client.get(groups_url, params: params, raise_errors: false)
|
||||||
|
|
||||||
if response.status == 200
|
if response.status == 200
|
||||||
response = response.parsed
|
response = response.parsed
|
||||||
groups.push(*response['groups'])
|
groups.push(*response["groups"])
|
||||||
page_token = response['nextPageToken']
|
page_token = response["nextPageToken"]
|
||||||
break if page_token.nil?
|
break if page_token.nil?
|
||||||
else
|
else
|
||||||
Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}")
|
Rails.logger.error(
|
||||||
|
"[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}",
|
||||||
|
)
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -107,26 +106,35 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator
|
|||||||
scope: GROUPS_SCOPE,
|
scope: GROUPS_SCOPE,
|
||||||
iat: Time.now.to_i,
|
iat: Time.now.to_i,
|
||||||
exp: Time.now.to_i + 60,
|
exp: Time.now.to_i + 60,
|
||||||
sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email
|
sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email,
|
||||||
}
|
}
|
||||||
headers = { "alg" => "RS256", "typ" => "JWT" }
|
headers = { "alg" => "RS256", "typ" => "JWT" }
|
||||||
key = OpenSSL::PKey::RSA.new(service_account_info["private_key"])
|
key = OpenSSL::PKey::RSA.new(service_account_info["private_key"])
|
||||||
|
|
||||||
encoded_jwt = ::JWT.encode(payload, key, 'RS256', headers)
|
encoded_jwt = ::JWT.encode(payload, key, "RS256", headers)
|
||||||
|
|
||||||
client = OAuth2::Client.new(
|
client =
|
||||||
SiteSetting.google_oauth2_client_id,
|
OAuth2::Client.new(
|
||||||
SiteSetting.google_oauth2_client_secret,
|
SiteSetting.google_oauth2_client_id,
|
||||||
site: OAUTH2_BASE_URL
|
SiteSetting.google_oauth2_client_secret,
|
||||||
)
|
site: OAUTH2_BASE_URL,
|
||||||
|
)
|
||||||
|
|
||||||
token_response = client.request(:post, '/token', body: {
|
token_response =
|
||||||
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
client.request(
|
||||||
assertion: encoded_jwt
|
:post,
|
||||||
}, raise_errors: false)
|
"/token",
|
||||||
|
body: {
|
||||||
|
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
||||||
|
assertion: encoded_jwt,
|
||||||
|
},
|
||||||
|
raise_errors: false,
|
||||||
|
)
|
||||||
|
|
||||||
if token_response.status != 200
|
if token_response.status != 200
|
||||||
Rails.logger.error("[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}")
|
Rails.logger.error(
|
||||||
|
"[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}",
|
||||||
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -56,28 +56,27 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
|
|
||||||
def after_authenticate(auth_token, existing_account: nil)
|
def after_authenticate(auth_token, existing_account: nil)
|
||||||
# Try and find an association for this account
|
# Try and find an association for this account
|
||||||
association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid])
|
association =
|
||||||
|
UserAssociatedAccount.find_or_initialize_by(
|
||||||
|
provider_name: auth_token[:provider],
|
||||||
|
provider_uid: auth_token[:uid],
|
||||||
|
)
|
||||||
|
|
||||||
# Reconnecting to existing account
|
# Reconnecting to existing account
|
||||||
if can_connect_existing_user? && existing_account && (association.user.nil? || existing_account.id != association.user_id)
|
if can_connect_existing_user? && existing_account &&
|
||||||
|
(association.user.nil? || existing_account.id != association.user_id)
|
||||||
association.user = existing_account
|
association.user = existing_account
|
||||||
end
|
end
|
||||||
|
|
||||||
# Matching an account by email
|
# Matching an account by email
|
||||||
if match_by_email &&
|
if match_by_email && association.user.nil? && (user = find_user_by_email(auth_token))
|
||||||
association.user.nil? &&
|
|
||||||
(user = find_user_by_email(auth_token))
|
|
||||||
|
|
||||||
UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user
|
UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user
|
||||||
association.user = user
|
association.user = user
|
||||||
end
|
end
|
||||||
|
|
||||||
# Matching an account by username
|
# Matching an account by username
|
||||||
if match_by_username &&
|
if match_by_username && association.user.nil? && SiteSetting.username_change_period.zero? &&
|
||||||
association.user.nil? &&
|
(user = find_user_by_username(auth_token))
|
||||||
SiteSetting.username_change_period.zero? &&
|
|
||||||
(user = find_user_by_username(auth_token))
|
|
||||||
|
|
||||||
UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user
|
UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user
|
||||||
association.user = user
|
association.user = user
|
||||||
end
|
end
|
||||||
@ -100,7 +99,14 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
result = Auth::Result.new
|
result = Auth::Result.new
|
||||||
info = auth_token[:info]
|
info = auth_token[:info]
|
||||||
result.email = info[:email]
|
result.email = info[:email]
|
||||||
result.name = (info[:first_name] && info[:last_name]) ? "#{info[:first_name]} #{info[:last_name]}" : info[:name]
|
result.name =
|
||||||
|
(
|
||||||
|
if (info[:first_name] && info[:last_name])
|
||||||
|
"#{info[:first_name]} #{info[:last_name]}"
|
||||||
|
else
|
||||||
|
info[:name]
|
||||||
|
end
|
||||||
|
)
|
||||||
if result.name.present? && result.name == result.email
|
if result.name.present? && result.name == result.email
|
||||||
# Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration)
|
# Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration)
|
||||||
# We add some generic protection here, so that users don't accidently make their email addresses public
|
# We add some generic protection here, so that users don't accidently make their email addresses public
|
||||||
@ -109,10 +115,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
result.username = info[:nickname]
|
result.username = info[:nickname]
|
||||||
result.email_valid = primary_email_verified?(auth_token) if result.email.present?
|
result.email_valid = primary_email_verified?(auth_token) if result.email.present?
|
||||||
result.overrides_email = always_update_user_email?
|
result.overrides_email = always_update_user_email?
|
||||||
result.extra_data = {
|
result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] }
|
||||||
provider: auth_token[:provider],
|
|
||||||
uid: auth_token[:uid]
|
|
||||||
}
|
|
||||||
result.user = association.user
|
result.user = association.user
|
||||||
|
|
||||||
result
|
result
|
||||||
@ -120,7 +123,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
|
|
||||||
def after_create_account(user, auth_result)
|
def after_create_account(user, auth_result)
|
||||||
auth_token = auth_result[:extra_data]
|
auth_token = auth_result[:extra_data]
|
||||||
association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid])
|
association =
|
||||||
|
UserAssociatedAccount.find_or_initialize_by(
|
||||||
|
provider_name: auth_token[:provider],
|
||||||
|
provider_uid: auth_token[:uid],
|
||||||
|
)
|
||||||
association.user = user
|
association.user = user
|
||||||
association.save!
|
association.save!
|
||||||
|
|
||||||
@ -132,16 +139,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
|
|
||||||
def find_user_by_email(auth_token)
|
def find_user_by_email(auth_token)
|
||||||
email = auth_token.dig(:info, :email)
|
email = auth_token.dig(:info, :email)
|
||||||
if email && primary_email_verified?(auth_token)
|
User.find_by_email(email) if email && primary_email_verified?(auth_token)
|
||||||
User.find_by_email(email)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_user_by_username(auth_token)
|
def find_user_by_username(auth_token)
|
||||||
username = auth_token.dig(:info, :nickname)
|
username = auth_token.dig(:info, :nickname)
|
||||||
if username
|
User.find_by_username(username) if username
|
||||||
User.find_by_username(username)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def retrieve_avatar(user, url)
|
def retrieve_avatar(user, url)
|
||||||
@ -158,7 +161,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator
|
|||||||
|
|
||||||
if bio || location
|
if bio || location
|
||||||
profile = user.user_profile
|
profile = user.user_profile
|
||||||
profile.bio_raw = bio unless profile.bio_raw.present?
|
profile.bio_raw = bio unless profile.bio_raw.present?
|
||||||
profile.location = location unless profile.location.present?
|
profile.location = location unless profile.location.present?
|
||||||
profile.save
|
profile.save
|
||||||
end
|
end
|
||||||
|
@ -1,48 +1,48 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Auth::Result
|
class Auth::Result
|
||||||
ATTRIBUTES = [
|
ATTRIBUTES = %i[
|
||||||
:user,
|
user
|
||||||
:name,
|
name
|
||||||
:username,
|
username
|
||||||
:email,
|
email
|
||||||
:email_valid,
|
email_valid
|
||||||
:extra_data,
|
extra_data
|
||||||
:awaiting_activation,
|
awaiting_activation
|
||||||
:awaiting_approval,
|
awaiting_approval
|
||||||
:authenticated,
|
authenticated
|
||||||
:authenticator_name,
|
authenticator_name
|
||||||
:requires_invite,
|
requires_invite
|
||||||
:not_allowed_from_ip_address,
|
not_allowed_from_ip_address
|
||||||
:admin_not_allowed_from_ip_address,
|
admin_not_allowed_from_ip_address
|
||||||
:skip_email_validation,
|
skip_email_validation
|
||||||
:destination_url,
|
destination_url
|
||||||
:omniauth_disallow_totp,
|
omniauth_disallow_totp
|
||||||
:failed,
|
failed
|
||||||
:failed_reason,
|
failed_reason
|
||||||
:failed_code,
|
failed_code
|
||||||
:associated_groups,
|
associated_groups
|
||||||
:overrides_email,
|
overrides_email
|
||||||
:overrides_username,
|
overrides_username
|
||||||
:overrides_name,
|
overrides_name
|
||||||
]
|
]
|
||||||
|
|
||||||
attr_accessor *ATTRIBUTES
|
attr_accessor *ATTRIBUTES
|
||||||
|
|
||||||
# These are stored in the session during
|
# These are stored in the session during
|
||||||
# account creation. The user cannot read or modify them
|
# account creation. The user cannot read or modify them
|
||||||
SESSION_ATTRIBUTES = [
|
SESSION_ATTRIBUTES = %i[
|
||||||
:email,
|
email
|
||||||
:username,
|
username
|
||||||
:email_valid,
|
email_valid
|
||||||
:name,
|
name
|
||||||
:authenticator_name,
|
authenticator_name
|
||||||
:extra_data,
|
extra_data
|
||||||
:skip_email_validation,
|
skip_email_validation
|
||||||
:associated_groups,
|
associated_groups
|
||||||
:overrides_email,
|
overrides_email
|
||||||
:overrides_username,
|
overrides_username
|
||||||
:overrides_name,
|
overrides_name
|
||||||
]
|
]
|
||||||
|
|
||||||
def [](key)
|
def [](key)
|
||||||
@ -59,9 +59,7 @@ class Auth::Result
|
|||||||
end
|
end
|
||||||
|
|
||||||
def email_valid=(val)
|
def email_valid=(val)
|
||||||
if !val.in? [true, false, nil]
|
raise ArgumentError, "email_valid should be boolean or nil" if !val.in? [true, false, nil]
|
||||||
raise ArgumentError, "email_valid should be boolean or nil"
|
|
||||||
end
|
|
||||||
@email_valid = !!val
|
@email_valid = !!val
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -83,14 +81,14 @@ class Auth::Result
|
|||||||
|
|
||||||
def apply_user_attributes!
|
def apply_user_attributes!
|
||||||
change_made = false
|
change_made = false
|
||||||
if (SiteSetting.auth_overrides_username? || overrides_username) && (resolved_username = resolve_username).present?
|
if (SiteSetting.auth_overrides_username? || overrides_username) &&
|
||||||
|
(resolved_username = resolve_username).present?
|
||||||
change_made = UsernameChanger.override(user, resolved_username)
|
change_made = UsernameChanger.override(user, resolved_username)
|
||||||
end
|
end
|
||||||
|
|
||||||
if (SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")) &&
|
if (
|
||||||
email_valid &&
|
SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")
|
||||||
email.present? &&
|
) && email_valid && email.present? && user.email != Email.downcase(email)
|
||||||
user.email != Email.downcase(email)
|
|
||||||
user.email = email
|
user.email = email
|
||||||
change_made = true
|
change_made = true
|
||||||
end
|
end
|
||||||
@ -109,11 +107,12 @@ class Auth::Result
|
|||||||
|
|
||||||
associated_groups.uniq.each do |associated_group|
|
associated_groups.uniq.each do |associated_group|
|
||||||
begin
|
begin
|
||||||
associated_group = AssociatedGroup.find_or_create_by(
|
associated_group =
|
||||||
name: associated_group[:name],
|
AssociatedGroup.find_or_create_by(
|
||||||
provider_id: associated_group[:id],
|
name: associated_group[:name],
|
||||||
provider_name: extra_data[:provider]
|
provider_id: associated_group[:id],
|
||||||
)
|
provider_name: extra_data[:provider],
|
||||||
|
)
|
||||||
rescue ActiveRecord::RecordNotUnique
|
rescue ActiveRecord::RecordNotUnique
|
||||||
retry
|
retry
|
||||||
end
|
end
|
||||||
@ -135,22 +134,12 @@ class Auth::Result
|
|||||||
end
|
end
|
||||||
|
|
||||||
def to_client_hash
|
def to_client_hash
|
||||||
if requires_invite
|
return { requires_invite: true } if requires_invite
|
||||||
return { requires_invite: true }
|
|
||||||
end
|
|
||||||
|
|
||||||
if user&.suspended?
|
return { suspended: true, suspended_message: user.suspended_message } if user&.suspended?
|
||||||
return {
|
|
||||||
suspended: true,
|
|
||||||
suspended_message: user.suspended_message
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
if omniauth_disallow_totp
|
if omniauth_disallow_totp
|
||||||
return {
|
return { omniauth_disallow_totp: !!omniauth_disallow_totp, email: email }
|
||||||
omniauth_disallow_totp: !!omniauth_disallow_totp,
|
|
||||||
email: email
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if user
|
if user
|
||||||
@ -159,7 +148,7 @@ class Auth::Result
|
|||||||
awaiting_activation: !!awaiting_activation,
|
awaiting_activation: !!awaiting_activation,
|
||||||
awaiting_approval: !!awaiting_approval,
|
awaiting_approval: !!awaiting_approval,
|
||||||
not_allowed_from_ip_address: !!not_allowed_from_ip_address,
|
not_allowed_from_ip_address: !!not_allowed_from_ip_address,
|
||||||
admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address
|
admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address,
|
||||||
}
|
}
|
||||||
|
|
||||||
result[:destination_url] = destination_url if authenticated && destination_url.present?
|
result[:destination_url] = destination_url if authenticated && destination_url.present?
|
||||||
@ -173,7 +162,7 @@ class Auth::Result
|
|||||||
auth_provider: authenticator_name,
|
auth_provider: authenticator_name,
|
||||||
email_valid: !!email_valid,
|
email_valid: !!email_valid,
|
||||||
can_edit_username: can_edit_username,
|
can_edit_username: can_edit_username,
|
||||||
can_edit_name: can_edit_name
|
can_edit_name: can_edit_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
result[:destination_url] = destination_url if destination_url.present?
|
result[:destination_url] = destination_url if destination_url.present?
|
||||||
@ -190,9 +179,7 @@ class Auth::Result
|
|||||||
|
|
||||||
def staged_user
|
def staged_user
|
||||||
return @staged_user if defined?(@staged_user)
|
return @staged_user if defined?(@staged_user)
|
||||||
if email.present? && email_valid
|
@staged_user = User.where(staged: true).find_by_email(email) if email.present? && email_valid
|
||||||
@staged_user = User.where(staged: true).find_by_email(email)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def username_suggester_attributes
|
def username_suggester_attributes
|
||||||
|
@ -17,11 +17,12 @@ class Auth::TwitterAuthenticator < Auth::ManagedAuthenticator
|
|||||||
|
|
||||||
def register_middleware(omniauth)
|
def register_middleware(omniauth)
|
||||||
omniauth.provider :twitter,
|
omniauth.provider :twitter,
|
||||||
setup: lambda { |env|
|
setup:
|
||||||
strategy = env["omniauth.strategy"]
|
lambda { |env|
|
||||||
strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key
|
strategy = env["omniauth.strategy"]
|
||||||
strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret
|
strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key
|
||||||
}
|
strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# twitter doesn't return unverfied email addresses in the API
|
# twitter doesn't return unverfied email addresses in the API
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Autospec
|
module Autospec
|
||||||
|
|
||||||
class BaseRunner
|
class BaseRunner
|
||||||
|
|
||||||
# used when starting the runner - preloading happens here
|
# used when starting the runner - preloading happens here
|
||||||
def start(opts = {})
|
def start(opts = {})
|
||||||
end
|
end
|
||||||
@ -32,7 +30,5 @@ module Autospec
|
|||||||
# used to stop the runner
|
# used to stop the runner
|
||||||
def stop
|
def stop
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,11 +3,15 @@
|
|||||||
require "rspec/core/formatters/base_text_formatter"
|
require "rspec/core/formatters/base_text_formatter"
|
||||||
require "parallel_tests/rspec/logger_base"
|
require "parallel_tests/rspec/logger_base"
|
||||||
|
|
||||||
module Autospec; end
|
module Autospec
|
||||||
|
end
|
||||||
|
|
||||||
class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
|
class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
|
||||||
|
RSpec::Core::Formatters.register self,
|
||||||
RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump
|
:example_passed,
|
||||||
|
:example_pending,
|
||||||
|
:example_failed,
|
||||||
|
:start_dump
|
||||||
|
|
||||||
RSPEC_RESULT = "./tmp/rspec_result"
|
RSPEC_RESULT = "./tmp/rspec_result"
|
||||||
|
|
||||||
@ -19,15 +23,15 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def example_passed(_notification)
|
def example_passed(_notification)
|
||||||
output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success)
|
output.print RSpec::Core::Formatters::ConsoleCodes.wrap(".", :success)
|
||||||
end
|
end
|
||||||
|
|
||||||
def example_pending(_notification)
|
def example_pending(_notification)
|
||||||
output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending)
|
output.print RSpec::Core::Formatters::ConsoleCodes.wrap("*", :pending)
|
||||||
end
|
end
|
||||||
|
|
||||||
def example_failed(notification)
|
def example_failed(notification)
|
||||||
output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure)
|
output.print RSpec::Core::Formatters::ConsoleCodes.wrap("F", :failure)
|
||||||
@fail_file.puts(notification.example.location + " ")
|
@fail_file.puts(notification.example.location + " ")
|
||||||
@fail_file.flush
|
@fail_file.flush
|
||||||
end
|
end
|
||||||
@ -40,5 +44,4 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter
|
|||||||
@fail_file.close
|
@fail_file.close
|
||||||
super(filename)
|
super(filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -7,7 +7,8 @@ require "autospec/reload_css"
|
|||||||
require "autospec/base_runner"
|
require "autospec/base_runner"
|
||||||
require "socket_server"
|
require "socket_server"
|
||||||
|
|
||||||
module Autospec; end
|
module Autospec
|
||||||
|
end
|
||||||
|
|
||||||
class Autospec::Manager
|
class Autospec::Manager
|
||||||
def self.run(opts = {})
|
def self.run(opts = {})
|
||||||
@ -25,7 +26,10 @@ class Autospec::Manager
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
Signal.trap("HUP") { stop_runners; exit }
|
Signal.trap("HUP") do
|
||||||
|
stop_runners
|
||||||
|
exit
|
||||||
|
end
|
||||||
|
|
||||||
Signal.trap("INT") do
|
Signal.trap("INT") do
|
||||||
begin
|
begin
|
||||||
@ -47,7 +51,6 @@ class Autospec::Manager
|
|||||||
STDIN.gets
|
STDIN.gets
|
||||||
process_queue
|
process_queue
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue => e
|
rescue => e
|
||||||
fail(e, "failed in run")
|
fail(e, "failed in run")
|
||||||
ensure
|
ensure
|
||||||
@ -71,16 +74,16 @@ class Autospec::Manager
|
|||||||
|
|
||||||
@queue.reject! { |_, s, _| s == "spec" }
|
@queue.reject! { |_, s, _| s == "spec" }
|
||||||
|
|
||||||
if current_runner
|
@queue.concat [["spec", "spec", current_runner]] if current_runner
|
||||||
@queue.concat [['spec', 'spec', current_runner]]
|
|
||||||
end
|
|
||||||
|
|
||||||
@runners.each do |runner|
|
@runners.each do |runner|
|
||||||
@queue.concat [['spec', 'spec', runner]] unless @queue.any? { |_, s, r| s == "spec" && r == runner }
|
unless @queue.any? { |_, s, r| s == "spec" && r == runner }
|
||||||
|
@queue.concat [["spec", "spec", runner]]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
[:start, :stop, :abort].each do |verb|
|
%i[start stop abort].each do |verb|
|
||||||
define_method("#{verb}_runners") do
|
define_method("#{verb}_runners") do
|
||||||
puts "@@@@@@@@@@@@ #{verb}_runners" if @debug
|
puts "@@@@@@@@@@@@ #{verb}_runners" if @debug
|
||||||
@runners.each(&verb)
|
@runners.each(&verb)
|
||||||
@ -89,11 +92,7 @@ class Autospec::Manager
|
|||||||
|
|
||||||
def start_service_queue
|
def start_service_queue
|
||||||
puts "@@@@@@@@@@@@ start_service_queue" if @debug
|
puts "@@@@@@@@@@@@ start_service_queue" if @debug
|
||||||
Thread.new do
|
Thread.new { thread_loop while true }
|
||||||
while true
|
|
||||||
thread_loop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# the main loop, will run the specs in the queue till one fails or the queue is empty
|
# the main loop, will run the specs in the queue till one fails or the queue is empty
|
||||||
@ -176,9 +175,7 @@ class Autospec::Manager
|
|||||||
Dir[root_path + "/plugins/*"].each do |f|
|
Dir[root_path + "/plugins/*"].each do |f|
|
||||||
next if !File.directory? f
|
next if !File.directory? f
|
||||||
resolved = File.realpath(f)
|
resolved = File.realpath(f)
|
||||||
if resolved != f
|
map[resolved] = f if resolved != f
|
||||||
map[resolved] = f
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
map
|
map
|
||||||
end
|
end
|
||||||
@ -188,9 +185,7 @@ class Autospec::Manager
|
|||||||
resolved = file
|
resolved = file
|
||||||
@reverse_map ||= reverse_symlink_map
|
@reverse_map ||= reverse_symlink_map
|
||||||
@reverse_map.each do |location, discourse_location|
|
@reverse_map.each do |location, discourse_location|
|
||||||
if file.start_with?(location)
|
resolved = discourse_location + file[location.length..-1] if file.start_with?(location)
|
||||||
resolved = discourse_location + file[location.length..-1]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
resolved
|
resolved
|
||||||
@ -199,9 +194,7 @@ class Autospec::Manager
|
|||||||
def listen_for_changes
|
def listen_for_changes
|
||||||
puts "@@@@@@@@@@@@ listen_for_changes" if @debug
|
puts "@@@@@@@@@@@@ listen_for_changes" if @debug
|
||||||
|
|
||||||
options = {
|
options = { ignore: %r{^lib/autospec} }
|
||||||
ignore: /^lib\/autospec/,
|
|
||||||
}
|
|
||||||
|
|
||||||
if @opts[:force_polling]
|
if @opts[:force_polling]
|
||||||
options[:force_polling] = true
|
options[:force_polling] = true
|
||||||
@ -210,14 +203,14 @@ class Autospec::Manager
|
|||||||
|
|
||||||
path = root_path
|
path = root_path
|
||||||
|
|
||||||
if ENV['VIM_AUTOSPEC']
|
if ENV["VIM_AUTOSPEC"]
|
||||||
STDERR.puts "Using VIM file listener"
|
STDERR.puts "Using VIM file listener"
|
||||||
|
|
||||||
socket_path = (Rails.root + "tmp/file_change.sock").to_s
|
socket_path = (Rails.root + "tmp/file_change.sock").to_s
|
||||||
FileUtils.rm_f(socket_path)
|
FileUtils.rm_f(socket_path)
|
||||||
server = SocketServer.new(socket_path)
|
server = SocketServer.new(socket_path)
|
||||||
server.start do |line|
|
server.start do |line|
|
||||||
file, line = line.split(' ')
|
file, line = line.split(" ")
|
||||||
file = reverse_symlink(file)
|
file = reverse_symlink(file)
|
||||||
file = file.sub(Rails.root.to_s + "/", "")
|
file = file.sub(Rails.root.to_s + "/", "")
|
||||||
# process_change can acquire a mutex and block
|
# process_change can acquire a mutex and block
|
||||||
@ -235,20 +228,20 @@ class Autospec::Manager
|
|||||||
end
|
end
|
||||||
|
|
||||||
# to speed up boot we use a thread
|
# to speed up boot we use a thread
|
||||||
["spec", "lib", "app", "config", "test", "vendor", "plugins"].each do |watch|
|
%w[spec lib app config test vendor plugins].each do |watch|
|
||||||
|
|
||||||
puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug
|
puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug
|
||||||
Thread.new do
|
Thread.new do
|
||||||
begin
|
begin
|
||||||
listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _|
|
listener =
|
||||||
paths = [modified, added].flatten
|
Listen.to("#{path}/#{watch}", options) do |modified, added, _|
|
||||||
paths.compact!
|
paths = [modified, added].flatten
|
||||||
paths.map! do |long|
|
paths.compact!
|
||||||
long = reverse_symlink(long)
|
paths.map! do |long|
|
||||||
long[(path.length + 1)..-1]
|
long = reverse_symlink(long)
|
||||||
|
long[(path.length + 1)..-1]
|
||||||
|
end
|
||||||
|
process_change(paths)
|
||||||
end
|
end
|
||||||
process_change(paths)
|
|
||||||
end
|
|
||||||
listener.start
|
listener.start
|
||||||
sleep
|
sleep
|
||||||
rescue => e
|
rescue => e
|
||||||
@ -257,7 +250,6 @@ class Autospec::Manager
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_change(files)
|
def process_change(files)
|
||||||
@ -285,13 +277,9 @@ class Autospec::Manager
|
|||||||
hit = true
|
hit = true
|
||||||
spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
|
spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file
|
||||||
with_line = spec
|
with_line = spec
|
||||||
if spec == file && line
|
with_line = spec + ":" << line.to_s if spec == file && line
|
||||||
with_line = spec + ":" << line.to_s
|
|
||||||
end
|
|
||||||
if File.exist?(spec) || Dir.exist?(spec)
|
if File.exist?(spec) || Dir.exist?(spec)
|
||||||
if with_line != spec
|
specs << [file, spec, runner] if with_line != spec
|
||||||
specs << [file, spec, runner]
|
|
||||||
end
|
|
||||||
specs << [file, with_line, runner]
|
specs << [file, with_line, runner]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -329,9 +317,7 @@ class Autospec::Manager
|
|||||||
focus = @queue.shift
|
focus = @queue.shift
|
||||||
@queue.unshift([file, spec, runner])
|
@queue.unshift([file, spec, runner])
|
||||||
unless spec.include?(":") && focus[1].include?(spec.split(":")[0])
|
unless spec.include?(":") && focus[1].include?(spec.split(":")[0])
|
||||||
if focus[1].include?(spec) || file != spec
|
@queue.unshift(focus) if focus[1].include?(spec) || file != spec
|
||||||
@queue.unshift(focus)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@queue.unshift([file, spec, runner])
|
@queue.unshift([file, spec, runner])
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Autospec; end
|
module Autospec
|
||||||
|
end
|
||||||
|
|
||||||
class Autospec::ReloadCss
|
class Autospec::ReloadCss
|
||||||
|
|
||||||
WATCHERS = {}
|
WATCHERS = {}
|
||||||
def self.watch(pattern, &blk)
|
def self.watch(pattern, &blk)
|
||||||
WATCHERS[pattern] = blk
|
WATCHERS[pattern] = blk
|
||||||
@ -30,7 +30,7 @@ class Autospec::ReloadCss
|
|||||||
if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ }
|
if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ }
|
||||||
# todo connect to dev instead?
|
# todo connect to dev instead?
|
||||||
ActiveRecord::Base.establish_connection
|
ActiveRecord::Base.establish_connection
|
||||||
[:desktop, :mobile].each do |style|
|
%i[desktop mobile].each do |style|
|
||||||
s = DiscourseStylesheets.new(style)
|
s = DiscourseStylesheets.new(style)
|
||||||
s.compile
|
s.compile
|
||||||
paths << "public" + s.stylesheet_relpath_no_digest
|
paths << "public" + s.stylesheet_relpath_no_digest
|
||||||
@ -44,10 +44,9 @@ class Autospec::ReloadCss
|
|||||||
p = p.sub(/\.sass\.erb/, "")
|
p = p.sub(/\.sass\.erb/, "")
|
||||||
p = p.sub(/\.sass/, "")
|
p = p.sub(/\.sass/, "")
|
||||||
p = p.sub(/\.scss/, "")
|
p = p.sub(/\.scss/, "")
|
||||||
p = p.sub(/^app\/assets\/stylesheets/, "assets")
|
p = p.sub(%r{^app/assets/stylesheets}, "assets")
|
||||||
{ name: p, hash: hash || SecureRandom.hex }
|
{ name: p, hash: hash || SecureRandom.hex }
|
||||||
end
|
end
|
||||||
message_bus.publish "/file-change", paths
|
message_bus.publish "/file-change", paths
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Autospec
|
module Autospec
|
||||||
|
|
||||||
class RspecRunner < BaseRunner
|
class RspecRunner < BaseRunner
|
||||||
|
|
||||||
WATCHERS = {}
|
WATCHERS = {}
|
||||||
def self.watch(pattern, &blk)
|
def self.watch(pattern, &blk)
|
||||||
WATCHERS[pattern] = blk
|
WATCHERS[pattern] = blk
|
||||||
@ -13,26 +11,28 @@ module Autospec
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Discourse specific
|
# Discourse specific
|
||||||
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
|
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" }
|
||||||
|
|
||||||
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
||||||
watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
|
||||||
watch(%r{^spec/.+_spec\.rb$})
|
watch(%r{^spec/.+_spec\.rb$})
|
||||||
watch(%r{^spec/support/.+\.rb$}) { "spec" }
|
watch(%r{^spec/support/.+\.rb$}) { "spec" }
|
||||||
watch("app/controllers/application_controller.rb") { "spec/requests" }
|
watch("app/controllers/application_controller.rb") { "spec/requests" }
|
||||||
|
|
||||||
watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
||||||
|
|
||||||
watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
||||||
|
|
||||||
watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" }
|
watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" }
|
||||||
|
|
||||||
watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" }
|
watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) do
|
||||||
|
"spec/components/pretty_text_spec.rb"
|
||||||
|
end
|
||||||
watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" }
|
watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" }
|
||||||
|
|
||||||
watch(%r{^plugins/.*/spec/.*\.rb})
|
watch(%r{^plugins/.*/spec/.*\.rb})
|
||||||
watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" }
|
watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" }
|
||||||
watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" }
|
watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" }
|
||||||
watch(%r{^(plugins/.*)/lib/(.*)\.rb}) { |m| "#{m[1]}/spec/lib/#{m[2]}_spec.rb" }
|
watch(%r{^(plugins/.*)/lib/(.*)\.rb}) { |m| "#{m[1]}/spec/lib/#{m[2]}_spec.rb" }
|
||||||
|
|
||||||
RELOADERS = Set.new
|
RELOADERS = Set.new
|
||||||
@ -50,11 +50,9 @@ module Autospec
|
|||||||
|
|
||||||
def failed_specs
|
def failed_specs
|
||||||
specs = []
|
specs = []
|
||||||
path = './tmp/rspec_result'
|
path = "./tmp/rspec_result"
|
||||||
specs = File.readlines(path) if File.exist?(path)
|
specs = File.readlines(path) if File.exist?(path)
|
||||||
specs
|
specs
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
require "autospec/rspec_runner"
|
require "autospec/rspec_runner"
|
||||||
|
|
||||||
module Autospec
|
module Autospec
|
||||||
|
|
||||||
class SimpleRunner < RspecRunner
|
class SimpleRunner < RspecRunner
|
||||||
def initialize
|
def initialize
|
||||||
@mutex = Mutex.new
|
@mutex = Mutex.new
|
||||||
@ -12,36 +11,29 @@ module Autospec
|
|||||||
def run(specs)
|
def run(specs)
|
||||||
puts "Running Rspec: #{specs}"
|
puts "Running Rspec: #{specs}"
|
||||||
# kill previous rspec instance
|
# kill previous rspec instance
|
||||||
@mutex.synchronize do
|
@mutex.synchronize { self.abort }
|
||||||
self.abort
|
|
||||||
end
|
|
||||||
# we use our custom rspec formatter
|
# we use our custom rspec formatter
|
||||||
args = [
|
args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter"]
|
||||||
"-r", "#{File.dirname(__FILE__)}/formatter.rb",
|
|
||||||
"-f", "Autospec::Formatter"
|
|
||||||
]
|
|
||||||
|
|
||||||
command = begin
|
command =
|
||||||
line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line
|
begin
|
||||||
multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files
|
line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line
|
||||||
if ENV["PARALLEL_SPEC"] == '1' && multiple_files && !line_specified
|
multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files
|
||||||
"bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}"
|
if ENV["PARALLEL_SPEC"] == "1" && multiple_files && !line_specified
|
||||||
else
|
"bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}"
|
||||||
"bin/rspec #{args.join(" ")} #{specs.split.join(" ")}"
|
else
|
||||||
|
"bin/rspec #{args.join(" ")} #{specs.split.join(" ")}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# launch rspec
|
# launch rspec
|
||||||
Dir.chdir(Rails.root) do # rubocop:disable Discourse/NoChdir because this is not part of the app
|
Dir.chdir(Rails.root) do # rubocop:disable Discourse/NoChdir because this is not part of the app
|
||||||
env = { "RAILS_ENV" => "test" }
|
env = { "RAILS_ENV" => "test" }
|
||||||
if specs.split(' ').any? { |s| s =~ /^(.\/)?plugins/ }
|
if specs.split(" ").any? { |s| s =~ %r{^(./)?plugins} }
|
||||||
env["LOAD_PLUGINS"] = "1"
|
env["LOAD_PLUGINS"] = "1"
|
||||||
puts "Loading plugins while running specs"
|
puts "Loading plugins while running specs"
|
||||||
end
|
end
|
||||||
pid =
|
pid = @mutex.synchronize { @pid = Process.spawn(env, command) }
|
||||||
@mutex.synchronize do
|
|
||||||
@pid = Process.spawn(env, command)
|
|
||||||
end
|
|
||||||
|
|
||||||
_, status = Process.wait2(pid)
|
_, status = Process.wait2(pid)
|
||||||
|
|
||||||
@ -51,7 +43,11 @@ module Autospec
|
|||||||
|
|
||||||
def abort
|
def abort
|
||||||
if pid = @pid
|
if pid = @pid
|
||||||
Process.kill("TERM", pid) rescue nil
|
begin
|
||||||
|
Process.kill("TERM", pid)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
wait_for_done(pid)
|
wait_for_done(pid)
|
||||||
pid = nil
|
pid = nil
|
||||||
end
|
end
|
||||||
@ -66,16 +62,26 @@ module Autospec
|
|||||||
|
|
||||||
def wait_for_done(pid)
|
def wait_for_done(pid)
|
||||||
i = 3000
|
i = 3000
|
||||||
while (i > 0 && Process.getpgid(pid) rescue nil)
|
while (
|
||||||
|
begin
|
||||||
|
i > 0 && Process.getpgid(pid)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
)
|
||||||
sleep 0.001
|
sleep 0.001
|
||||||
i -= 1
|
i -= 1
|
||||||
end
|
end
|
||||||
if (Process.getpgid(pid) rescue nil)
|
if (
|
||||||
|
begin
|
||||||
|
Process.getpgid(pid)
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
)
|
||||||
STDERR.puts "Terminating rspec #{pid} by force cause it refused graceful termination"
|
STDERR.puts "Terminating rspec #{pid} by force cause it refused graceful termination"
|
||||||
Process.kill("KILL", pid)
|
Process.kill("KILL", pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module BackupRestore
|
module BackupRestore
|
||||||
|
class OperationRunningError < RuntimeError
|
||||||
class OperationRunningError < RuntimeError; end
|
end
|
||||||
|
|
||||||
VERSION_PREFIX = "v"
|
VERSION_PREFIX = "v"
|
||||||
DUMP_FILE = "dump.sql.gz"
|
DUMP_FILE = "dump.sql.gz"
|
||||||
@ -22,9 +22,7 @@ module BackupRestore
|
|||||||
|
|
||||||
def self.rollback!
|
def self.rollback!
|
||||||
raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running?
|
raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running?
|
||||||
if can_rollback?
|
move_tables_between_schemas("backup", "public") if can_rollback?
|
||||||
move_tables_between_schemas("backup", "public")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cancel!
|
def self.cancel!
|
||||||
@ -58,7 +56,7 @@ module BackupRestore
|
|||||||
{
|
{
|
||||||
is_operation_running: is_operation_running?,
|
is_operation_running: is_operation_running?,
|
||||||
can_rollback: can_rollback?,
|
can_rollback: can_rollback?,
|
||||||
allow_restore: Rails.env.development? || SiteSetting.allow_restore
|
allow_restore: Rails.env.development? || SiteSetting.allow_restore,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -133,7 +131,7 @@ module BackupRestore
|
|||||||
config["backup_port"] || config["port"],
|
config["backup_port"] || config["port"],
|
||||||
config["username"] || username || ENV["USER"] || "postgres",
|
config["username"] || username || ENV["USER"] || "postgres",
|
||||||
config["password"] || password,
|
config["password"] || password,
|
||||||
config["database"]
|
config["database"],
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -194,7 +192,11 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.backup_tables_count
|
def self.backup_tables_count
|
||||||
DB.query_single("SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'").first.to_i
|
DB
|
||||||
|
.query_single(
|
||||||
|
"SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'",
|
||||||
|
)
|
||||||
|
.first
|
||||||
|
.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -69,15 +69,22 @@ module BackupRestore
|
|||||||
path_transformation =
|
path_transformation =
|
||||||
case tar_implementation
|
case tar_implementation
|
||||||
when :gnu
|
when :gnu
|
||||||
['--transform', 's|var/www/discourse/public/uploads/|uploads/|']
|
%w[--transform s|var/www/discourse/public/uploads/|uploads/|]
|
||||||
when :bsd
|
when :bsd
|
||||||
['-s', '|var/www/discourse/public/uploads/|uploads/|']
|
%w[-s |var/www/discourse/public/uploads/|uploads/|]
|
||||||
end
|
end
|
||||||
|
|
||||||
log "Unzipping archive, this may take a while..."
|
log "Unzipping archive, this may take a while..."
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'tar', '--extract', '--gzip', '--file', @archive_path, '--directory', @tmp_directory,
|
"tar",
|
||||||
*path_transformation, failure_message: "Failed to decompress archive."
|
"--extract",
|
||||||
|
"--gzip",
|
||||||
|
"--file",
|
||||||
|
@archive_path,
|
||||||
|
"--directory",
|
||||||
|
@tmp_directory,
|
||||||
|
*path_transformation,
|
||||||
|
failure_message: "Failed to decompress archive.",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -86,15 +93,19 @@ module BackupRestore
|
|||||||
if @is_archive
|
if @is_archive
|
||||||
# for compatibility with backups from Discourse v1.5 and below
|
# for compatibility with backups from Discourse v1.5 and below
|
||||||
old_dump_path = File.join(@tmp_directory, OLD_DUMP_FILENAME)
|
old_dump_path = File.join(@tmp_directory, OLD_DUMP_FILENAME)
|
||||||
File.exist?(old_dump_path) ? old_dump_path : File.join(@tmp_directory, BackupRestore::DUMP_FILE)
|
if File.exist?(old_dump_path)
|
||||||
|
old_dump_path
|
||||||
|
else
|
||||||
|
File.join(@tmp_directory, BackupRestore::DUMP_FILE)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
File.join(@tmp_directory, @filename)
|
File.join(@tmp_directory, @filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
if File.extname(@db_dump_path) == '.gz'
|
if File.extname(@db_dump_path) == ".gz"
|
||||||
log "Extracting dump file..."
|
log "Extracting dump file..."
|
||||||
Compression::Gzip.new.decompress(@tmp_directory, @db_dump_path, available_size)
|
Compression::Gzip.new.decompress(@tmp_directory, @db_dump_path, available_size)
|
||||||
@db_dump_path.delete_suffix!('.gz')
|
@db_dump_path.delete_suffix!(".gz")
|
||||||
end
|
end
|
||||||
|
|
||||||
@db_dump_path
|
@db_dump_path
|
||||||
@ -105,17 +116,18 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def tar_implementation
|
def tar_implementation
|
||||||
@tar_version ||= begin
|
@tar_version ||=
|
||||||
tar_version = Discourse::Utils.execute_command('tar', '--version')
|
begin
|
||||||
|
tar_version = Discourse::Utils.execute_command("tar", "--version")
|
||||||
|
|
||||||
if tar_version.include?("GNU tar")
|
if tar_version.include?("GNU tar")
|
||||||
:gnu
|
:gnu
|
||||||
elsif tar_version.include?("bsdtar")
|
elsif tar_version.include?("bsdtar")
|
||||||
:bsd
|
:bsd
|
||||||
else
|
else
|
||||||
raise "Unknown tar implementation: #{tar_version}"
|
raise "Unknown tar implementation: #{tar_version}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -37,9 +37,7 @@ module BackupRestore
|
|||||||
return unless cleanup_allowed?
|
return unless cleanup_allowed?
|
||||||
return if (backup_files = files).size <= SiteSetting.maximum_backups
|
return if (backup_files = files).size <= SiteSetting.maximum_backups
|
||||||
|
|
||||||
backup_files[SiteSetting.maximum_backups..-1].each do |file|
|
backup_files[SiteSetting.maximum_backups..-1].each { |file| delete_file(file.filename) }
|
||||||
delete_file(file.filename)
|
|
||||||
end
|
|
||||||
|
|
||||||
reset_cache
|
reset_cache
|
||||||
end
|
end
|
||||||
@ -74,7 +72,7 @@ module BackupRestore
|
|||||||
used_bytes: used_bytes,
|
used_bytes: used_bytes,
|
||||||
free_bytes: free_bytes,
|
free_bytes: free_bytes,
|
||||||
count: files.size,
|
count: files.size,
|
||||||
last_backup_taken_at: latest_file&.last_modified
|
last_backup_taken_at: latest_file&.last_modified,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ require "mini_mime"
|
|||||||
require "file_store/s3_store"
|
require "file_store/s3_store"
|
||||||
|
|
||||||
module BackupRestore
|
module BackupRestore
|
||||||
|
|
||||||
class Backuper
|
class Backuper
|
||||||
attr_reader :success
|
attr_reader :success
|
||||||
|
|
||||||
@ -84,7 +83,11 @@ module BackupRestore
|
|||||||
@dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE)
|
@dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE)
|
||||||
@archive_directory = BackupRestore::LocalBackupStore.base_directory(db: @current_db)
|
@archive_directory = BackupRestore::LocalBackupStore.base_directory(db: @current_db)
|
||||||
filename = @filename_override || "#{get_parameterized_title}-#{@timestamp}"
|
filename = @filename_override || "#{get_parameterized_title}-#{@timestamp}"
|
||||||
@archive_basename = File.join(@archive_directory, "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}")
|
@archive_basename =
|
||||||
|
File.join(
|
||||||
|
@archive_directory,
|
||||||
|
"#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}",
|
||||||
|
)
|
||||||
|
|
||||||
@backup_filename =
|
@backup_filename =
|
||||||
if @with_uploads
|
if @with_uploads
|
||||||
@ -119,9 +122,18 @@ module BackupRestore
|
|||||||
BackupMetadata.delete_all
|
BackupMetadata.delete_all
|
||||||
BackupMetadata.create!(name: "base_url", value: Discourse.base_url)
|
BackupMetadata.create!(name: "base_url", value: Discourse.base_url)
|
||||||
BackupMetadata.create!(name: "cdn_url", value: Discourse.asset_host)
|
BackupMetadata.create!(name: "cdn_url", value: Discourse.asset_host)
|
||||||
BackupMetadata.create!(name: "s3_base_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil)
|
BackupMetadata.create!(
|
||||||
BackupMetadata.create!(name: "s3_cdn_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil)
|
name: "s3_base_url",
|
||||||
BackupMetadata.create!(name: "db_name", value: RailsMultisite::ConnectionManagement.current_db)
|
value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil,
|
||||||
|
)
|
||||||
|
BackupMetadata.create!(
|
||||||
|
name: "s3_cdn_url",
|
||||||
|
value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil,
|
||||||
|
)
|
||||||
|
BackupMetadata.create!(
|
||||||
|
name: "db_name",
|
||||||
|
value: RailsMultisite::ConnectionManagement.current_db,
|
||||||
|
)
|
||||||
BackupMetadata.create!(name: "multisite", value: Rails.configuration.multisite)
|
BackupMetadata.create!(name: "multisite", value: Rails.configuration.multisite)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -132,7 +144,7 @@ module BackupRestore
|
|||||||
pg_dump_running = true
|
pg_dump_running = true
|
||||||
|
|
||||||
Thread.new do
|
Thread.new do
|
||||||
RailsMultisite::ConnectionManagement::establish_connection(db: @current_db)
|
RailsMultisite::ConnectionManagement.establish_connection(db: @current_db)
|
||||||
while pg_dump_running
|
while pg_dump_running
|
||||||
message = logs.pop.strip
|
message = logs.pop.strip
|
||||||
log(message) unless message.blank?
|
log(message) unless message.blank?
|
||||||
@ -159,23 +171,24 @@ module BackupRestore
|
|||||||
db_conf = BackupRestore.database_configuration
|
db_conf = BackupRestore.database_configuration
|
||||||
|
|
||||||
password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present?
|
password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present?
|
||||||
host_argument = "--host=#{db_conf.host}" if db_conf.host.present?
|
host_argument = "--host=#{db_conf.host}" if db_conf.host.present?
|
||||||
port_argument = "--port=#{db_conf.port}" if db_conf.port.present?
|
port_argument = "--port=#{db_conf.port}" if db_conf.port.present?
|
||||||
username_argument = "--username=#{db_conf.username}" if db_conf.username.present?
|
username_argument = "--username=#{db_conf.username}" if db_conf.username.present?
|
||||||
|
|
||||||
[ password_argument, # pass the password to pg_dump (if any)
|
[
|
||||||
"pg_dump", # the pg_dump command
|
password_argument, # pass the password to pg_dump (if any)
|
||||||
"--schema=public", # only public schema
|
"pg_dump", # the pg_dump command
|
||||||
"-T public.pg_*", # exclude tables and views whose name starts with "pg_"
|
"--schema=public", # only public schema
|
||||||
|
"-T public.pg_*", # exclude tables and views whose name starts with "pg_"
|
||||||
"--file='#{@dump_filename}'", # output to the dump.sql file
|
"--file='#{@dump_filename}'", # output to the dump.sql file
|
||||||
"--no-owner", # do not output commands to set ownership of objects
|
"--no-owner", # do not output commands to set ownership of objects
|
||||||
"--no-privileges", # prevent dumping of access privileges
|
"--no-privileges", # prevent dumping of access privileges
|
||||||
"--verbose", # specifies verbose mode
|
"--verbose", # specifies verbose mode
|
||||||
"--compress=4", # Compression level of 4
|
"--compress=4", # Compression level of 4
|
||||||
host_argument, # the hostname to connect to (if any)
|
host_argument, # the hostname to connect to (if any)
|
||||||
port_argument, # the port to connect to (if any)
|
port_argument, # the port to connect to (if any)
|
||||||
username_argument, # the username to connect as (if any)
|
username_argument, # the username to connect as (if any)
|
||||||
db_conf.database # the name of the database to dump
|
db_conf.database, # the name of the database to dump
|
||||||
].join(" ")
|
].join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -185,8 +198,10 @@ module BackupRestore
|
|||||||
archive_filename = File.join(@archive_directory, @backup_filename)
|
archive_filename = File.join(@archive_directory, @backup_filename)
|
||||||
|
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'mv', @dump_filename, archive_filename,
|
"mv",
|
||||||
failure_message: "Failed to move database dump file."
|
@dump_filename,
|
||||||
|
archive_filename,
|
||||||
|
failure_message: "Failed to move database dump file.",
|
||||||
)
|
)
|
||||||
|
|
||||||
remove_tmp_directory
|
remove_tmp_directory
|
||||||
@ -198,17 +213,29 @@ module BackupRestore
|
|||||||
tar_filename = "#{@archive_basename}.tar"
|
tar_filename = "#{@archive_basename}.tar"
|
||||||
|
|
||||||
log "Making sure archive does not already exist..."
|
log "Making sure archive does not already exist..."
|
||||||
Discourse::Utils.execute_command('rm', '-f', tar_filename)
|
Discourse::Utils.execute_command("rm", "-f", tar_filename)
|
||||||
Discourse::Utils.execute_command('rm', '-f', "#{tar_filename}.gz")
|
Discourse::Utils.execute_command("rm", "-f", "#{tar_filename}.gz")
|
||||||
|
|
||||||
log "Creating empty archive..."
|
log "Creating empty archive..."
|
||||||
Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, '--files-from', '/dev/null')
|
Discourse::Utils.execute_command(
|
||||||
|
"tar",
|
||||||
|
"--create",
|
||||||
|
"--file",
|
||||||
|
tar_filename,
|
||||||
|
"--files-from",
|
||||||
|
"/dev/null",
|
||||||
|
)
|
||||||
|
|
||||||
log "Archiving data dump..."
|
log "Archiving data dump..."
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'tar', '--append', '--dereference', '--file', tar_filename, File.basename(@dump_filename),
|
"tar",
|
||||||
|
"--append",
|
||||||
|
"--dereference",
|
||||||
|
"--file",
|
||||||
|
tar_filename,
|
||||||
|
File.basename(@dump_filename),
|
||||||
failure_message: "Failed to archive data dump.",
|
failure_message: "Failed to archive data dump.",
|
||||||
chdir: File.dirname(@dump_filename)
|
chdir: File.dirname(@dump_filename),
|
||||||
)
|
)
|
||||||
|
|
||||||
add_local_uploads_to_archive(tar_filename)
|
add_local_uploads_to_archive(tar_filename)
|
||||||
@ -218,8 +245,10 @@ module BackupRestore
|
|||||||
|
|
||||||
log "Gzipping archive, this may take a while..."
|
log "Gzipping archive, this may take a while..."
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'gzip', "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", tar_filename,
|
"gzip",
|
||||||
failure_message: "Failed to gzip archive."
|
"-#{SiteSetting.backup_gzip_compression_level_for_uploads}",
|
||||||
|
tar_filename,
|
||||||
|
failure_message: "Failed to gzip archive.",
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -244,14 +273,21 @@ module BackupRestore
|
|||||||
if SiteSetting.include_thumbnails_in_backups
|
if SiteSetting.include_thumbnails_in_backups
|
||||||
exclude_optimized = ""
|
exclude_optimized = ""
|
||||||
else
|
else
|
||||||
optimized_path = File.join(upload_directory, 'optimized')
|
optimized_path = File.join(upload_directory, "optimized")
|
||||||
exclude_optimized = "--exclude=#{optimized_path}"
|
exclude_optimized = "--exclude=#{optimized_path}"
|
||||||
end
|
end
|
||||||
|
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'tar', '--append', '--dereference', exclude_optimized, '--file', tar_filename, upload_directory,
|
"tar",
|
||||||
failure_message: "Failed to archive uploads.", success_status_codes: [0, 1],
|
"--append",
|
||||||
chdir: File.join(Rails.root, "public")
|
"--dereference",
|
||||||
|
exclude_optimized,
|
||||||
|
"--file",
|
||||||
|
tar_filename,
|
||||||
|
upload_directory,
|
||||||
|
failure_message: "Failed to archive uploads.",
|
||||||
|
success_status_codes: [0, 1],
|
||||||
|
chdir: File.join(Rails.root, "public"),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
log "No local uploads found. Skipping archiving of local uploads..."
|
log "No local uploads found. Skipping archiving of local uploads..."
|
||||||
@ -287,9 +323,14 @@ module BackupRestore
|
|||||||
|
|
||||||
log "Appending uploads to archive..."
|
log "Appending uploads to archive..."
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command(
|
||||||
'tar', '--append', '--file', tar_filename, upload_directory,
|
"tar",
|
||||||
failure_message: "Failed to append uploads to archive.", success_status_codes: [0, 1],
|
"--append",
|
||||||
chdir: @tmp_directory
|
"--file",
|
||||||
|
tar_filename,
|
||||||
|
upload_directory,
|
||||||
|
failure_message: "Failed to append uploads to archive.",
|
||||||
|
success_status_codes: [0, 1],
|
||||||
|
chdir: @tmp_directory,
|
||||||
)
|
)
|
||||||
|
|
||||||
log "No uploads found on S3. Skipping archiving of uploads stored on S3..." if count == 0
|
log "No uploads found on S3. Skipping archiving of uploads stored on S3..." if count == 0
|
||||||
@ -327,9 +368,7 @@ module BackupRestore
|
|||||||
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
logs = Discourse::Utils.logs_markdown(@logs, user: @user)
|
||||||
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
post = SystemMessage.create_from_system_user(@user, status, logs: logs)
|
||||||
|
|
||||||
if @user.id == Discourse::SYSTEM_USER_ID
|
post.topic.invite_group(@user, Group[:admins]) if @user.id == Discourse::SYSTEM_USER_ID
|
||||||
post.topic.invite_group(@user, Group[:admins])
|
|
||||||
end
|
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while notifying user.", ex
|
log "Something went wrong while notifying user.", ex
|
||||||
end
|
end
|
||||||
@ -399,7 +438,12 @@ module BackupRestore
|
|||||||
def publish_log(message, timestamp)
|
def publish_log(message, timestamp)
|
||||||
return unless @publish_to_message_bus
|
return unless @publish_to_message_bus
|
||||||
data = { timestamp: timestamp, operation: "backup", message: message }
|
data = { timestamp: timestamp, operation: "backup", message: message }
|
||||||
MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id])
|
MessageBus.publish(
|
||||||
|
BackupRestore::LOGS_CHANNEL,
|
||||||
|
data,
|
||||||
|
user_ids: [@user_id],
|
||||||
|
client_ids: [@client_id],
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_log(message, timestamp)
|
def save_log(message, timestamp)
|
||||||
@ -416,5 +460,4 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -46,9 +46,7 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.drop_backup_schema
|
def self.drop_backup_schema
|
||||||
if backup_schema_dropable?
|
ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) if backup_schema_dropable?
|
||||||
ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.core_migration_files
|
def self.core_migration_files
|
||||||
@ -65,13 +63,14 @@ module BackupRestore
|
|||||||
last_line = nil
|
last_line = nil
|
||||||
psql_running = true
|
psql_running = true
|
||||||
|
|
||||||
log_thread = Thread.new do
|
log_thread =
|
||||||
RailsMultisite::ConnectionManagement::establish_connection(db: @current_db)
|
Thread.new do
|
||||||
while psql_running || !logs.empty?
|
RailsMultisite::ConnectionManagement.establish_connection(db: @current_db)
|
||||||
message = logs.pop.strip
|
while psql_running || !logs.empty?
|
||||||
log(message) if message.present?
|
message = logs.pop.strip
|
||||||
|
log(message) if message.present?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
IO.popen(restore_dump_command) do |pipe|
|
IO.popen(restore_dump_command) do |pipe|
|
||||||
begin
|
begin
|
||||||
@ -89,7 +88,9 @@ module BackupRestore
|
|||||||
logs << ""
|
logs << ""
|
||||||
log_thread.join
|
log_thread.join
|
||||||
|
|
||||||
raise DatabaseRestoreError.new("psql failed: #{last_line}") if Process.last_status&.exitstatus != 0
|
if Process.last_status&.exitstatus != 0
|
||||||
|
raise DatabaseRestoreError.new("psql failed: #{last_line}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Removes unwanted SQL added by certain versions of pg_dump and modifies
|
# Removes unwanted SQL added by certain versions of pg_dump and modifies
|
||||||
@ -99,7 +100,7 @@ module BackupRestore
|
|||||||
"DROP SCHEMA", # Discourse <= v1.5
|
"DROP SCHEMA", # Discourse <= v1.5
|
||||||
"CREATE SCHEMA", # PostgreSQL 11+
|
"CREATE SCHEMA", # PostgreSQL 11+
|
||||||
"COMMENT ON SCHEMA", # PostgreSQL 11+
|
"COMMENT ON SCHEMA", # PostgreSQL 11+
|
||||||
"SET default_table_access_method" # PostgreSQL 12
|
"SET default_table_access_method", # PostgreSQL 12
|
||||||
].join("|")
|
].join("|")
|
||||||
|
|
||||||
command = "sed -E '/^(#{unwanted_sql})/d' #{@db_dump_path}"
|
command = "sed -E '/^(#{unwanted_sql})/d' #{@db_dump_path}"
|
||||||
@ -117,18 +118,19 @@ module BackupRestore
|
|||||||
db_conf = BackupRestore.database_configuration
|
db_conf = BackupRestore.database_configuration
|
||||||
|
|
||||||
password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present?
|
password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present?
|
||||||
host_argument = "--host=#{db_conf.host}" if db_conf.host.present?
|
host_argument = "--host=#{db_conf.host}" if db_conf.host.present?
|
||||||
port_argument = "--port=#{db_conf.port}" if db_conf.port.present?
|
port_argument = "--port=#{db_conf.port}" if db_conf.port.present?
|
||||||
username_argument = "--username=#{db_conf.username}" if db_conf.username.present?
|
username_argument = "--username=#{db_conf.username}" if db_conf.username.present?
|
||||||
|
|
||||||
[ password_argument, # pass the password to psql (if any)
|
[
|
||||||
"psql", # the psql command
|
password_argument, # pass the password to psql (if any)
|
||||||
|
"psql", # the psql command
|
||||||
"--dbname='#{db_conf.database}'", # connect to database *dbname*
|
"--dbname='#{db_conf.database}'", # connect to database *dbname*
|
||||||
"--single-transaction", # all or nothing (also runs COPY commands faster)
|
"--single-transaction", # all or nothing (also runs COPY commands faster)
|
||||||
"--variable=ON_ERROR_STOP=1", # stop on first error
|
"--variable=ON_ERROR_STOP=1", # stop on first error
|
||||||
host_argument, # the hostname to connect to (if any)
|
host_argument, # the hostname to connect to (if any)
|
||||||
port_argument, # the port to connect to (if any)
|
port_argument, # the port to connect to (if any)
|
||||||
username_argument # the username to connect as (if any)
|
username_argument, # the username to connect as (if any)
|
||||||
].compact.join(" ")
|
].compact.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -136,21 +138,22 @@ module BackupRestore
|
|||||||
log "Migrating the database..."
|
log "Migrating the database..."
|
||||||
|
|
||||||
log Discourse::Utils.execute_command(
|
log Discourse::Utils.execute_command(
|
||||||
{
|
{
|
||||||
"SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0",
|
"SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0",
|
||||||
"SKIP_OPTIMIZE_ICONS" => "1",
|
"SKIP_OPTIMIZE_ICONS" => "1",
|
||||||
"DISABLE_TRANSLATION_OVERRIDES" => "1"
|
"DISABLE_TRANSLATION_OVERRIDES" => "1",
|
||||||
},
|
},
|
||||||
"rake", "db:migrate",
|
"rake",
|
||||||
failure_message: "Failed to migrate database.",
|
"db:migrate",
|
||||||
chdir: Rails.root
|
failure_message: "Failed to migrate database.",
|
||||||
)
|
chdir: Rails.root,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def reconnect_database
|
def reconnect_database
|
||||||
log "Reconnecting to the database..."
|
log "Reconnecting to the database..."
|
||||||
RailsMultisite::ConnectionManagement::reload if RailsMultisite::ConnectionManagement::instance
|
RailsMultisite::ConnectionManagement.reload if RailsMultisite::ConnectionManagement.instance
|
||||||
RailsMultisite::ConnectionManagement::establish_connection(db: @current_db)
|
RailsMultisite::ConnectionManagement.establish_connection(db: @current_db)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_missing_discourse_functions
|
def create_missing_discourse_functions
|
||||||
@ -179,10 +182,12 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
existing_function_names = Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" }
|
existing_function_names =
|
||||||
|
Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" }
|
||||||
|
|
||||||
all_readonly_table_columns.each do |table_name, column_name|
|
all_readonly_table_columns.each do |table_name, column_name|
|
||||||
function_name = Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false)
|
function_name =
|
||||||
|
Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false)
|
||||||
|
|
||||||
if !existing_function_names.include?(function_name)
|
if !existing_function_names.include?(function_name)
|
||||||
Migration::BaseDropper.create_readonly_function(table_name, column_name)
|
Migration::BaseDropper.create_readonly_function(table_name, column_name)
|
||||||
|
@ -12,7 +12,12 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.chunk_path(identifier, filename, chunk_number)
|
def self.chunk_path(identifier, filename, chunk_number)
|
||||||
File.join(LocalBackupStore.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}")
|
File.join(
|
||||||
|
LocalBackupStore.base_directory,
|
||||||
|
"tmp",
|
||||||
|
identifier,
|
||||||
|
"#{filename}.part#{chunk_number}",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(opts = {})
|
def initialize(opts = {})
|
||||||
@ -39,7 +44,7 @@ module BackupRestore
|
|||||||
|
|
||||||
def download_file(filename, destination, failure_message = "")
|
def download_file(filename, destination, failure_message = "")
|
||||||
path = path_from_filename(filename)
|
path = path_from_filename(filename)
|
||||||
Discourse::Utils.execute_command('cp', path, destination, failure_message: failure_message)
|
Discourse::Utils.execute_command("cp", path, destination, failure_message: failure_message)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -59,7 +64,7 @@ module BackupRestore
|
|||||||
filename: File.basename(path),
|
filename: File.basename(path),
|
||||||
size: File.size(path),
|
size: File.size(path),
|
||||||
last_modified: File.mtime(path).utc,
|
last_modified: File.mtime(path).utc,
|
||||||
source: include_download_source ? path : nil
|
source: include_download_source ? path : nil,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -32,7 +32,12 @@ module BackupRestore
|
|||||||
def publish_log(message, timestamp)
|
def publish_log(message, timestamp)
|
||||||
return unless @publish_to_message_bus
|
return unless @publish_to_message_bus
|
||||||
data = { timestamp: timestamp, operation: "restore", message: message }
|
data = { timestamp: timestamp, operation: "restore", message: message }
|
||||||
MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id])
|
MessageBus.publish(
|
||||||
|
BackupRestore::LOGS_CHANNEL,
|
||||||
|
data,
|
||||||
|
user_ids: [@user_id],
|
||||||
|
client_ids: [@client_id],
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def save_log(message, timestamp)
|
def save_log(message, timestamp)
|
||||||
|
@ -28,8 +28,10 @@ module BackupRestore
|
|||||||
log " Restored version: #{metadata[:version]}"
|
log " Restored version: #{metadata[:version]}"
|
||||||
|
|
||||||
if metadata[:version] > @current_version
|
if metadata[:version] > @current_version
|
||||||
raise MigrationRequiredError.new("You're trying to restore a more recent version of the schema. " \
|
raise MigrationRequiredError.new(
|
||||||
"You should migrate first!")
|
"You're trying to restore a more recent version of the schema. " \
|
||||||
|
"You should migrate first!",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
metadata
|
metadata
|
||||||
|
@ -65,8 +65,8 @@ module BackupRestore
|
|||||||
|
|
||||||
after_restore_hook
|
after_restore_hook
|
||||||
rescue Compression::Strategy::ExtractFailed
|
rescue Compression::Strategy::ExtractFailed
|
||||||
log 'ERROR: The uncompressed file is too big. Consider increasing the hidden ' \
|
log "ERROR: The uncompressed file is too big. Consider increasing the hidden " \
|
||||||
'"decompressed_backup_max_file_size_mb" setting.'
|
'"decompressed_backup_max_file_size_mb" setting.'
|
||||||
@database_restorer.rollback
|
@database_restorer.rollback
|
||||||
rescue SystemExit
|
rescue SystemExit
|
||||||
log "Restore process was cancelled!"
|
log "Restore process was cancelled!"
|
||||||
@ -118,10 +118,10 @@ module BackupRestore
|
|||||||
|
|
||||||
DiscourseEvent.trigger(:site_settings_restored)
|
DiscourseEvent.trigger(:site_settings_restored)
|
||||||
|
|
||||||
if @disable_emails && SiteSetting.disable_emails == 'no'
|
if @disable_emails && SiteSetting.disable_emails == "no"
|
||||||
log "Disabling outgoing emails for non-staff users..."
|
log "Disabling outgoing emails for non-staff users..."
|
||||||
user = User.find_by_email(@user_info[:email]) || Discourse.system_user
|
user = User.find_by_email(@user_info[:email]) || Discourse.system_user
|
||||||
SiteSetting.set_and_log(:disable_emails, 'non-staff', user)
|
SiteSetting.set_and_log(:disable_emails, "non-staff", user)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -152,7 +152,7 @@ module BackupRestore
|
|||||||
post = SystemMessage.create_from_system_user(user, status, logs: logs)
|
post = SystemMessage.create_from_system_user(user, status, logs: logs)
|
||||||
else
|
else
|
||||||
log "Could not send notification to '#{@user_info[:username]}' " \
|
log "Could not send notification to '#{@user_info[:username]}' " \
|
||||||
"(#{@user_info[:email]}), because the user does not exist."
|
"(#{@user_info[:email]}), because the user does not exist."
|
||||||
end
|
end
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while notifying user.", ex
|
log "Something went wrong while notifying user.", ex
|
||||||
|
@ -4,8 +4,11 @@ module BackupRestore
|
|||||||
class S3BackupStore < BackupStore
|
class S3BackupStore < BackupStore
|
||||||
UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 6.hours.to_i
|
UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 6.hours.to_i
|
||||||
|
|
||||||
delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts,
|
delegate :abort_multipart,
|
||||||
:complete_multipart, to: :s3_helper
|
:presign_multipart_part,
|
||||||
|
:list_multipart_parts,
|
||||||
|
:complete_multipart,
|
||||||
|
to: :s3_helper
|
||||||
|
|
||||||
def initialize(opts = {})
|
def initialize(opts = {})
|
||||||
@s3_options = S3Helper.s3_options(SiteSetting)
|
@s3_options = S3Helper.s3_options(SiteSetting)
|
||||||
@ -13,7 +16,7 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def s3_helper
|
def s3_helper
|
||||||
@s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, '', @s3_options.clone)
|
@s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, "", @s3_options.clone)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remote?
|
def remote?
|
||||||
@ -57,11 +60,17 @@ module BackupRestore
|
|||||||
|
|
||||||
presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS)
|
presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS)
|
||||||
rescue Aws::Errors::ServiceError => e
|
rescue Aws::Errors::ServiceError => e
|
||||||
Rails.logger.warn("Failed to generate upload URL for S3: #{e.message.presence || e.class.name}")
|
Rails.logger.warn(
|
||||||
|
"Failed to generate upload URL for S3: #{e.message.presence || e.class.name}",
|
||||||
|
)
|
||||||
raise StorageError.new(e.message.presence || e.class.name)
|
raise StorageError.new(e.message.presence || e.class.name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {})
|
def signed_url_for_temporary_upload(
|
||||||
|
file_name,
|
||||||
|
expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS,
|
||||||
|
metadata: {}
|
||||||
|
)
|
||||||
obj = object_from_path(file_name)
|
obj = object_from_path(file_name)
|
||||||
raise BackupFileExists.new if obj.exists?
|
raise BackupFileExists.new if obj.exists?
|
||||||
key = temporary_upload_path(file_name)
|
key = temporary_upload_path(file_name)
|
||||||
@ -71,8 +80,8 @@ module BackupRestore
|
|||||||
expires_in: expires_in,
|
expires_in: expires_in,
|
||||||
opts: {
|
opts: {
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
acl: "private"
|
acl: "private",
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -84,7 +93,7 @@ module BackupRestore
|
|||||||
folder_prefix = s3_helper.s3_bucket_folder_path.nil? ? "" : s3_helper.s3_bucket_folder_path
|
folder_prefix = s3_helper.s3_bucket_folder_path.nil? ? "" : s3_helper.s3_bucket_folder_path
|
||||||
|
|
||||||
if Rails.env.test?
|
if Rails.env.test?
|
||||||
folder_prefix = File.join(folder_prefix, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}")
|
folder_prefix = File.join(folder_prefix, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}")
|
||||||
end
|
end
|
||||||
|
|
||||||
folder_prefix
|
folder_prefix
|
||||||
@ -105,7 +114,10 @@ module BackupRestore
|
|||||||
s3_helper.copy(
|
s3_helper.copy(
|
||||||
existing_external_upload_key,
|
existing_external_upload_key,
|
||||||
File.join(s3_helper.s3_bucket_folder_path, original_filename),
|
File.join(s3_helper.s3_bucket_folder_path, original_filename),
|
||||||
options: { acl: "private", apply_metadata_to_destination: true }
|
options: {
|
||||||
|
acl: "private",
|
||||||
|
apply_metadata_to_destination: true,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
s3_helper.delete_object(existing_external_upload_key)
|
s3_helper.delete_object(existing_external_upload_key)
|
||||||
end
|
end
|
||||||
@ -120,9 +132,7 @@ module BackupRestore
|
|||||||
objects = []
|
objects = []
|
||||||
|
|
||||||
s3_helper.list.each do |obj|
|
s3_helper.list.each do |obj|
|
||||||
if obj.key.match?(file_regex)
|
objects << create_file_from_object(obj) if obj.key.match?(file_regex)
|
||||||
objects << create_file_from_object(obj)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
objects
|
objects
|
||||||
@ -137,7 +147,7 @@ module BackupRestore
|
|||||||
filename: File.basename(obj.key),
|
filename: File.basename(obj.key),
|
||||||
size: obj.size,
|
size: obj.size,
|
||||||
last_modified: obj.last_modified,
|
last_modified: obj.last_modified,
|
||||||
source: include_download_source ? presigned_url(obj, :get, expires) : nil
|
source: include_download_source ? presigned_url(obj, :get, expires) : nil,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -154,16 +164,17 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def file_regex
|
def file_regex
|
||||||
@file_regex ||= begin
|
@file_regex ||=
|
||||||
path = s3_helper.s3_bucket_folder_path || ""
|
begin
|
||||||
|
path = s3_helper.s3_bucket_folder_path || ""
|
||||||
|
|
||||||
if path.present?
|
if path.present?
|
||||||
path = "#{path}/" unless path.end_with?("/")
|
path = "#{path}/" unless path.end_with?("/")
|
||||||
path = Regexp.quote(path)
|
path = Regexp.quote(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
%r{^#{path}[^/]*\.t?gz$}i
|
||||||
end
|
end
|
||||||
|
|
||||||
/^#{path}[^\/]*\.t?gz$/i
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def free_bytes
|
def free_bytes
|
||||||
|
@ -98,9 +98,7 @@ module BackupRestore
|
|||||||
|
|
||||||
def flush_redis
|
def flush_redis
|
||||||
redis = Discourse.redis
|
redis = Discourse.redis
|
||||||
redis.scan_each(match: "*") do |key|
|
redis.scan_each(match: "*") { |key| redis.del(key) unless key == SidekiqPauser::PAUSED_KEY }
|
||||||
redis.del(key) unless key == SidekiqPauser::PAUSED_KEY
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_sidekiq_queues
|
def clear_sidekiq_queues
|
||||||
|
@ -11,11 +11,12 @@ module BackupRestore
|
|||||||
def self.s3_regex_string(s3_base_url)
|
def self.s3_regex_string(s3_base_url)
|
||||||
clean_url = s3_base_url.sub(S3_ENDPOINT_REGEX, ".s3.amazonaws.com")
|
clean_url = s3_base_url.sub(S3_ENDPOINT_REGEX, ".s3.amazonaws.com")
|
||||||
|
|
||||||
regex_string = clean_url
|
regex_string =
|
||||||
.split(".s3.amazonaws.com")
|
clean_url
|
||||||
.map { |s| Regexp.escape(s) }
|
.split(".s3.amazonaws.com")
|
||||||
.insert(1, S3_ENDPOINT_REGEX.source)
|
.map { |s| Regexp.escape(s) }
|
||||||
.join("")
|
.insert(1, S3_ENDPOINT_REGEX.source)
|
||||||
|
.join("")
|
||||||
|
|
||||||
[regex_string, clean_url]
|
[regex_string, clean_url]
|
||||||
end
|
end
|
||||||
@ -25,12 +26,16 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
|
|
||||||
def restore(tmp_directory)
|
def restore(tmp_directory)
|
||||||
upload_directories = Dir.glob(File.join(tmp_directory, "uploads", "*"))
|
upload_directories =
|
||||||
.reject { |path| File.basename(path).start_with?("PaxHeaders") }
|
Dir
|
||||||
|
.glob(File.join(tmp_directory, "uploads", "*"))
|
||||||
|
.reject { |path| File.basename(path).start_with?("PaxHeaders") }
|
||||||
|
|
||||||
if upload_directories.count > 1
|
if upload_directories.count > 1
|
||||||
raise UploadsRestoreError.new("Could not find uploads, because the uploads " \
|
raise UploadsRestoreError.new(
|
||||||
"directory contains multiple folders.")
|
"Could not find uploads, because the uploads " \
|
||||||
|
"directory contains multiple folders.",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@tmp_uploads_path = upload_directories.first
|
@tmp_uploads_path = upload_directories.first
|
||||||
@ -55,7 +60,9 @@ module BackupRestore
|
|||||||
if !store.respond_to?(:copy_from)
|
if !store.respond_to?(:copy_from)
|
||||||
# a FileStore implementation from a plugin might not support this method, so raise a helpful error
|
# a FileStore implementation from a plugin might not support this method, so raise a helpful error
|
||||||
store_name = Discourse.store.class.name
|
store_name = Discourse.store.class.name
|
||||||
raise UploadsRestoreError.new("The current file store (#{store_name}) does not support restoring uploads.")
|
raise UploadsRestoreError.new(
|
||||||
|
"The current file store (#{store_name}) does not support restoring uploads.",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
log "Restoring uploads, this may take a while..."
|
log "Restoring uploads, this may take a while..."
|
||||||
@ -89,13 +96,17 @@ module BackupRestore
|
|||||||
remap(old_base_url, Discourse.base_url)
|
remap(old_base_url, Discourse.base_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
current_s3_base_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil
|
current_s3_base_url =
|
||||||
if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && old_s3_base_url != current_s3_base_url
|
SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil
|
||||||
|
if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) &&
|
||||||
|
old_s3_base_url != current_s3_base_url
|
||||||
remap_s3("#{old_s3_base_url}/", uploads_folder)
|
remap_s3("#{old_s3_base_url}/", uploads_folder)
|
||||||
end
|
end
|
||||||
|
|
||||||
current_s3_cdn_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil
|
current_s3_cdn_url =
|
||||||
if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && old_s3_cdn_url != current_s3_cdn_url
|
SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil
|
||||||
|
if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) &&
|
||||||
|
old_s3_cdn_url != current_s3_cdn_url
|
||||||
base_url = current_s3_cdn_url || Discourse.base_url
|
base_url = current_s3_cdn_url || Discourse.base_url
|
||||||
remap("#{old_s3_cdn_url}/", UrlHelper.schemaless("#{base_url}#{uploads_folder}"))
|
remap("#{old_s3_cdn_url}/", UrlHelper.schemaless("#{base_url}#{uploads_folder}"))
|
||||||
|
|
||||||
@ -113,10 +124,7 @@ module BackupRestore
|
|||||||
remap(old_host, new_host) if old_host != new_host
|
remap(old_host, new_host) if old_host != new_host
|
||||||
end
|
end
|
||||||
|
|
||||||
if @previous_db_name != @current_db_name
|
remap("/uploads/#{@previous_db_name}/", upload_path) if @previous_db_name != @current_db_name
|
||||||
remap("/uploads/#{@previous_db_name}/", upload_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
rescue => ex
|
rescue => ex
|
||||||
log "Something went wrong while remapping uploads.", ex
|
log "Something went wrong while remapping uploads.", ex
|
||||||
end
|
end
|
||||||
@ -130,7 +138,12 @@ module BackupRestore
|
|||||||
if old_s3_base_url.include?("amazonaws.com")
|
if old_s3_base_url.include?("amazonaws.com")
|
||||||
from_regex, from_clean_url = self.class.s3_regex_string(old_s3_base_url)
|
from_regex, from_clean_url = self.class.s3_regex_string(old_s3_base_url)
|
||||||
log "Remapping with regex from '#{from_clean_url}' to '#{uploads_folder}'"
|
log "Remapping with regex from '#{from_clean_url}' to '#{uploads_folder}'"
|
||||||
DbHelper.regexp_replace(from_regex, uploads_folder, verbose: true, excluded_tables: ["backup_metadata"])
|
DbHelper.regexp_replace(
|
||||||
|
from_regex,
|
||||||
|
uploads_folder,
|
||||||
|
verbose: true,
|
||||||
|
excluded_tables: ["backup_metadata"],
|
||||||
|
)
|
||||||
else
|
else
|
||||||
remap(old_s3_base_url, uploads_folder)
|
remap(old_s3_base_url, uploads_folder)
|
||||||
end
|
end
|
||||||
@ -141,13 +154,15 @@ module BackupRestore
|
|||||||
DB.exec("TRUNCATE TABLE optimized_images")
|
DB.exec("TRUNCATE TABLE optimized_images")
|
||||||
SiteIconManager.ensure_optimized!
|
SiteIconManager.ensure_optimized!
|
||||||
|
|
||||||
User.where("uploaded_avatar_id IS NOT NULL").find_each do |user|
|
User
|
||||||
Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id)
|
.where("uploaded_avatar_id IS NOT NULL")
|
||||||
end
|
.find_each do |user|
|
||||||
|
Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rebake_posts_with_uploads
|
def rebake_posts_with_uploads
|
||||||
log 'Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed.'
|
log "Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed."
|
||||||
log 'You can expedite the process by manually running "rake posts:rebake_uncooked_posts"'
|
log 'You can expedite the process by manually running "rake posts:rebake_uncooked_posts"'
|
||||||
|
|
||||||
DB.exec(<<~SQL)
|
DB.exec(<<~SQL)
|
||||||
|
@ -173,7 +173,7 @@ module BadgeQueries
|
|||||||
<<~SQL
|
<<~SQL
|
||||||
SELECT p.user_id, p.id post_id, current_timestamp granted_at
|
SELECT p.user_id, p.id post_id, current_timestamp granted_at
|
||||||
FROM badge_posts p
|
FROM badge_posts p
|
||||||
WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND
|
WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1"} AND p.like_count >= #{count.to_i} AND
|
||||||
(:backfill OR p.id IN (:post_ids) )
|
(:backfill OR p.id IN (:post_ids) )
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
@ -271,5 +271,4 @@ module BadgeQueries
|
|||||||
WHERE "rank" = 1
|
WHERE "rank" = 1
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -11,9 +11,7 @@ class BookmarkQuery
|
|||||||
|
|
||||||
def self.preload(bookmarks, object)
|
def self.preload(bookmarks, object)
|
||||||
preload_polymorphic_associations(bookmarks, object.guardian)
|
preload_polymorphic_associations(bookmarks, object.guardian)
|
||||||
if @preload
|
@preload.each { |preload| preload.call(bookmarks, object) } if @preload
|
||||||
@preload.each { |preload| preload.call(bookmarks, object) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# These polymorphic associations are loaded to make the UserBookmarkListSerializer's
|
# These polymorphic associations are loaded to make the UserBookmarkListSerializer's
|
||||||
@ -42,24 +40,27 @@ class BookmarkQuery
|
|||||||
ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil
|
ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil
|
||||||
search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil
|
search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil
|
||||||
|
|
||||||
queries = Bookmark.registered_bookmarkables.map do |bookmarkable|
|
queries =
|
||||||
interim_results = bookmarkable.perform_list_query(@user, @guardian)
|
Bookmark
|
||||||
|
.registered_bookmarkables
|
||||||
|
.map do |bookmarkable|
|
||||||
|
interim_results = bookmarkable.perform_list_query(@user, @guardian)
|
||||||
|
|
||||||
# this could occur if there is some security reason that the user cannot
|
# this could occur if there is some security reason that the user cannot
|
||||||
# access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark
|
# access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark
|
||||||
# on a topic and that topic was moved into a private category
|
# on a topic and that topic was moved into a private category
|
||||||
next if interim_results.blank?
|
next if interim_results.blank?
|
||||||
|
|
||||||
if search_term.present?
|
if search_term.present?
|
||||||
interim_results = bookmarkable.perform_search_query(
|
interim_results =
|
||||||
interim_results, search_term_wildcard, ts_query
|
bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query)
|
||||||
)
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# this is purely to make the query easy to read and debug, otherwise it's
|
# this is purely to make the query easy to read and debug, otherwise it's
|
||||||
# all mashed up into a massive ball in MiniProfiler :)
|
# all mashed up into a massive ball in MiniProfiler :)
|
||||||
"---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}"
|
"---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}"
|
||||||
end.compact
|
end
|
||||||
|
.compact
|
||||||
|
|
||||||
# same for interim results being blank, the user might have been locked out
|
# same for interim results being blank, the user might have been locked out
|
||||||
# from all their various bookmarks, in which case they will see nothing and
|
# from all their various bookmarks, in which case they will see nothing and
|
||||||
@ -68,17 +69,16 @@ class BookmarkQuery
|
|||||||
|
|
||||||
union_sql = queries.join("\n\nUNION\n\n")
|
union_sql = queries.join("\n\nUNION\n\n")
|
||||||
results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks")
|
results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks")
|
||||||
results = results.order(
|
results =
|
||||||
"(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END),
|
results.order(
|
||||||
|
"(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END),
|
||||||
bookmarks.reminder_at ASC,
|
bookmarks.reminder_at ASC,
|
||||||
bookmarks.updated_at DESC"
|
bookmarks.updated_at DESC",
|
||||||
)
|
)
|
||||||
|
|
||||||
@count = results.count
|
@count = results.count
|
||||||
|
|
||||||
if @page.positive?
|
results = results.offset(@page * @params[:per_page]) if @page.positive?
|
||||||
results = results.offset(@page * @params[:per_page])
|
|
||||||
end
|
|
||||||
|
|
||||||
if updated_results = blk&.call(results)
|
if updated_results = blk&.call(results)
|
||||||
results = updated_results
|
results = updated_results
|
||||||
|
@ -28,12 +28,10 @@ class BookmarkReminderNotificationHandler
|
|||||||
|
|
||||||
def clear_reminder
|
def clear_reminder
|
||||||
Rails.logger.debug(
|
Rails.logger.debug(
|
||||||
"Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}"
|
"Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}",
|
||||||
)
|
)
|
||||||
|
|
||||||
if bookmark.auto_clear_reminder_when_reminder_sent?
|
bookmark.reminder_at = nil if bookmark.auto_clear_reminder_when_reminder_sent?
|
||||||
bookmark.reminder_at = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
bookmark.clear_reminder!
|
bookmark.clear_reminder!
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module BrowserDetection
|
module BrowserDetection
|
||||||
|
|
||||||
def self.browser(user_agent)
|
def self.browser(user_agent)
|
||||||
case user_agent
|
case user_agent
|
||||||
when /Edg/i
|
when /Edg/i
|
||||||
@ -66,5 +65,4 @@ module BrowserDetection
|
|||||||
:unknown
|
:unknown
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
17
lib/cache.rb
17
lib/cache.rb
@ -15,12 +15,11 @@
|
|||||||
# this makes it harder to reason about the API
|
# this makes it harder to reason about the API
|
||||||
|
|
||||||
class Cache
|
class Cache
|
||||||
|
|
||||||
# nothing is cached for longer than 1 day EVER
|
# nothing is cached for longer than 1 day EVER
|
||||||
# there is no reason to have data older than this clogging redis
|
# there is no reason to have data older than this clogging redis
|
||||||
# it is dangerous cause if we rename keys we will be stuck with
|
# it is dangerous cause if we rename keys we will be stuck with
|
||||||
# pointless data
|
# pointless data
|
||||||
MAX_CACHE_AGE = 1.day unless defined? MAX_CACHE_AGE
|
MAX_CACHE_AGE = 1.day unless defined?(MAX_CACHE_AGE)
|
||||||
|
|
||||||
attr_reader :namespace
|
attr_reader :namespace
|
||||||
|
|
||||||
@ -47,9 +46,7 @@ class Cache
|
|||||||
end
|
end
|
||||||
|
|
||||||
def clear
|
def clear
|
||||||
keys.each do |k|
|
keys.each { |k| redis.del(k) }
|
||||||
redis.del(k)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_key(key)
|
def normalize_key(key)
|
||||||
@ -80,9 +77,7 @@ class Cache
|
|||||||
key = normalize_key(name)
|
key = normalize_key(name)
|
||||||
raw = nil
|
raw = nil
|
||||||
|
|
||||||
if !force
|
raw = redis.get(key) if !force
|
||||||
raw = redis.get(key)
|
|
||||||
end
|
|
||||||
|
|
||||||
if raw
|
if raw
|
||||||
begin
|
begin
|
||||||
@ -96,7 +91,8 @@ class Cache
|
|||||||
val
|
val
|
||||||
end
|
end
|
||||||
elsif force
|
elsif force
|
||||||
raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
|
raise ArgumentError,
|
||||||
|
"Missing block: Calling `Cache#fetch` with `force: true` requires a block."
|
||||||
else
|
else
|
||||||
read(name)
|
read(name)
|
||||||
end
|
end
|
||||||
@ -105,7 +101,7 @@ class Cache
|
|||||||
protected
|
protected
|
||||||
|
|
||||||
def log_first_exception(e)
|
def log_first_exception(e)
|
||||||
if !defined? @logged_a_warning
|
if !defined?(@logged_a_warning)
|
||||||
@logged_a_warning = true
|
@logged_a_warning = true
|
||||||
Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}")
|
Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}")
|
||||||
end
|
end
|
||||||
@ -129,5 +125,4 @@ class Cache
|
|||||||
redis.setex(key, expiry, dumped)
|
redis.setex(key, expiry, dumped)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
module CanonicalURL
|
module CanonicalURL
|
||||||
module ControllerExtensions
|
module ControllerExtensions
|
||||||
ALLOWED_CANONICAL_PARAMS = %w(page)
|
ALLOWED_CANONICAL_PARAMS = %w[page]
|
||||||
|
|
||||||
def canonical_url(url_for_options = {})
|
def canonical_url(url_for_options = {})
|
||||||
case url_for_options
|
case url_for_options
|
||||||
@ -14,14 +14,15 @@ module CanonicalURL
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_canonical
|
def default_canonical
|
||||||
@default_canonical ||= begin
|
@default_canonical ||=
|
||||||
canonical = +"#{Discourse.base_url_no_prefix}#{request.path}"
|
begin
|
||||||
allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) }
|
canonical = +"#{Discourse.base_url_no_prefix}#{request.path}"
|
||||||
if allowed_params.present?
|
allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) }
|
||||||
canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}"
|
if allowed_params.present?
|
||||||
|
canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}"
|
||||||
|
end
|
||||||
|
canonical
|
||||||
end
|
end
|
||||||
canonical
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
@ -31,7 +32,7 @@ module CanonicalURL
|
|||||||
|
|
||||||
module Helpers
|
module Helpers
|
||||||
def canonical_link_tag(url = nil)
|
def canonical_link_tag(url = nil)
|
||||||
tag('link', rel: 'canonical', href: url || @canonical_url || default_canonical)
|
tag("link", rel: "canonical", href: url || @canonical_url || default_canonical)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,23 +1,26 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module CategoryBadge
|
module CategoryBadge
|
||||||
|
|
||||||
def self.category_stripe(color, classes)
|
def self.category_stripe(color, classes)
|
||||||
style = color ? "style='background-color: ##{color};'" : ''
|
style = color ? "style='background-color: ##{color};'" : ""
|
||||||
"<span class='#{classes}' #{style}></span>"
|
"<span class='#{classes}' #{style}></span>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.inline_category_stripe(color, styles = '', insert_blank = false)
|
def self.inline_category_stripe(color, styles = "", insert_blank = false)
|
||||||
"<span style='background-color: ##{color};#{styles}'>#{insert_blank ? ' ' : ''}</span>"
|
"<span style='background-color: ##{color};#{styles}'>#{insert_blank ? " " : ""}</span>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.inline_badge_wrapper_style(category)
|
def self.inline_badge_wrapper_style(category)
|
||||||
style =
|
style =
|
||||||
case (SiteSetting.category_style || :box).to_sym
|
case (SiteSetting.category_style || :box).to_sym
|
||||||
when :bar then 'line-height: 1.25; margin-right: 5px;'
|
when :bar
|
||||||
when :box then "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;"
|
"line-height: 1.25; margin-right: 5px;"
|
||||||
when :bullet then 'line-height: 1; margin-right: 10px;'
|
when :box
|
||||||
when :none then ''
|
"background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;"
|
||||||
|
when :bullet
|
||||||
|
"line-height: 1; margin-right: 10px;"
|
||||||
|
when :none
|
||||||
|
""
|
||||||
end
|
end
|
||||||
|
|
||||||
" style='font-size: 0.857em; white-space: nowrap; display: inline-block; position: relative; #{style}'"
|
" style='font-size: 0.857em; white-space: nowrap; display: inline-block; position: relative; #{style}'"
|
||||||
@ -34,73 +37,88 @@ module CategoryBadge
|
|||||||
|
|
||||||
extra_classes = "#{opts[:extra_classes]} #{SiteSetting.category_style}"
|
extra_classes = "#{opts[:extra_classes]} #{SiteSetting.category_style}"
|
||||||
|
|
||||||
result = +''
|
result = +""
|
||||||
|
|
||||||
# parent span
|
# parent span
|
||||||
unless category.parent_category_id.nil? || opts[:hide_parent]
|
unless category.parent_category_id.nil? || opts[:hide_parent]
|
||||||
parent_category = Category.find_by(id: category.parent_category_id)
|
parent_category = Category.find_by(id: category.parent_category_id)
|
||||||
result <<
|
result << if opts[:inline_style]
|
||||||
if opts[:inline_style]
|
case (SiteSetting.category_style || :box).to_sym
|
||||||
case (SiteSetting.category_style || :box).to_sym
|
when :bar
|
||||||
when :bar
|
inline_category_stripe(
|
||||||
inline_category_stripe(parent_category.color, 'display: inline-block; padding: 1px;', true)
|
parent_category.color,
|
||||||
when :box
|
"display: inline-block; padding: 1px;",
|
||||||
inline_category_stripe(parent_category.color, 'display: inline-block; padding: 0 1px;', true)
|
true,
|
||||||
when :bullet
|
)
|
||||||
inline_category_stripe(parent_category.color, 'display: inline-block; width: 5px; height: 10px; line-height: 1;')
|
when :box
|
||||||
when :none
|
inline_category_stripe(
|
||||||
''
|
parent_category.color,
|
||||||
end
|
"display: inline-block; padding: 0 1px;",
|
||||||
else
|
true,
|
||||||
category_stripe(parent_category.color, 'badge-category-parent-bg')
|
)
|
||||||
|
when :bullet
|
||||||
|
inline_category_stripe(
|
||||||
|
parent_category.color,
|
||||||
|
"display: inline-block; width: 5px; height: 10px; line-height: 1;",
|
||||||
|
)
|
||||||
|
when :none
|
||||||
|
""
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
category_stripe(parent_category.color, "badge-category-parent-bg")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# sub parent or main category span
|
# sub parent or main category span
|
||||||
result <<
|
result << if opts[:inline_style]
|
||||||
if opts[:inline_style]
|
case (SiteSetting.category_style || :box).to_sym
|
||||||
case (SiteSetting.category_style || :box).to_sym
|
when :bar
|
||||||
when :bar
|
inline_category_stripe(category.color, "display: inline-block; padding: 1px;", true)
|
||||||
inline_category_stripe(category.color, 'display: inline-block; padding: 1px;', true)
|
when :box
|
||||||
when :box
|
""
|
||||||
''
|
when :bullet
|
||||||
when :bullet
|
inline_category_stripe(
|
||||||
inline_category_stripe(category.color, "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;")
|
category.color,
|
||||||
when :none
|
"display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;",
|
||||||
''
|
)
|
||||||
end
|
when :none
|
||||||
else
|
""
|
||||||
category_stripe(category.color, 'badge-category-bg')
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
category_stripe(category.color, "badge-category-bg")
|
||||||
|
end
|
||||||
|
|
||||||
# category name
|
# category name
|
||||||
class_names = 'badge-category clear-badge'
|
class_names = "badge-category clear-badge"
|
||||||
description = category.description_text ? "title='#{category.description_text}'" : ''
|
description = category.description_text ? "title='#{category.description_text}'" : ""
|
||||||
category_url = opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url
|
category_url =
|
||||||
|
opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url
|
||||||
|
|
||||||
extra_span_classes =
|
extra_span_classes =
|
||||||
if opts[:inline_style]
|
if opts[:inline_style]
|
||||||
case (SiteSetting.category_style || :box).to_sym
|
case (SiteSetting.category_style || :box).to_sym
|
||||||
when :bar
|
when :bar
|
||||||
'color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;'
|
"color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;"
|
||||||
when :box
|
when :box
|
||||||
"color: ##{category.text_color}; padding: 0 5px;"
|
"color: ##{category.text_color}; padding: 0 5px;"
|
||||||
when :bullet
|
when :bullet
|
||||||
'color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;'
|
"color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;"
|
||||||
when :none
|
when :none
|
||||||
''
|
""
|
||||||
end + 'max-width: 150px; overflow: hidden; text-overflow: ellipsis;'
|
end + "max-width: 150px; overflow: hidden; text-overflow: ellipsis;"
|
||||||
elsif (SiteSetting.category_style).to_sym == :box
|
elsif (SiteSetting.category_style).to_sym == :box
|
||||||
"color: ##{category.text_color}"
|
"color: ##{category.text_color}"
|
||||||
else
|
else
|
||||||
''
|
""
|
||||||
end
|
end
|
||||||
result << "<span style='#{extra_span_classes}' data-drop-close='true' class='#{class_names}'
|
result << "<span style='#{extra_span_classes}' data-drop-close='true' class='#{class_names}'
|
||||||
#{description}>"
|
#{description}>"
|
||||||
|
|
||||||
result << ERB::Util.html_escape(category.name) << '</span>'
|
result << ERB::Util.html_escape(category.name) << "</span>"
|
||||||
|
|
||||||
result = "<a class='badge-wrapper #{extra_classes}' href='#{category_url}'" + (opts[:inline_style] ? inline_badge_wrapper_style(category) : '') + ">#{result}</a>"
|
result =
|
||||||
|
"<a class='badge-wrapper #{extra_classes}' href='#{category_url}'" +
|
||||||
|
(opts[:inline_style] ? inline_badge_wrapper_style(category) : "") + ">#{result}</a>"
|
||||||
|
|
||||||
result.html_safe
|
result.html_safe
|
||||||
end
|
end
|
||||||
|
@ -3,13 +3,17 @@
|
|||||||
require "rbconfig"
|
require "rbconfig"
|
||||||
|
|
||||||
class ChromeInstalledChecker
|
class ChromeInstalledChecker
|
||||||
class ChromeError < StandardError; end
|
class ChromeError < StandardError
|
||||||
class ChromeVersionError < ChromeError; end
|
end
|
||||||
class ChromeNotInstalled < ChromeError; end
|
class ChromeVersionError < ChromeError
|
||||||
class ChromeVersionTooLow < ChromeError; end
|
end
|
||||||
|
class ChromeNotInstalled < ChromeError
|
||||||
|
end
|
||||||
|
class ChromeVersionTooLow < ChromeError
|
||||||
|
end
|
||||||
|
|
||||||
def self.run
|
def self.run
|
||||||
if RbConfig::CONFIG['host_os'][/darwin|mac os/]
|
if RbConfig::CONFIG["host_os"][/darwin|mac os/]
|
||||||
binary = "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
|
binary = "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome"
|
||||||
elsif system("command -v google-chrome-stable >/dev/null;")
|
elsif system("command -v google-chrome-stable >/dev/null;")
|
||||||
binary = "google-chrome-stable"
|
binary = "google-chrome-stable"
|
||||||
@ -18,15 +22,15 @@ class ChromeInstalledChecker
|
|||||||
binary ||= "chromium" if system("command -v chromium >/dev/null;")
|
binary ||= "chromium" if system("command -v chromium >/dev/null;")
|
||||||
|
|
||||||
if !binary
|
if !binary
|
||||||
raise ChromeNotInstalled.new("Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html")
|
raise ChromeNotInstalled.new(
|
||||||
|
"Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
version = `\"#{binary}\" --version`
|
version = `\"#{binary}\" --version`
|
||||||
version_match = version.match(/[\d\.]+/)
|
version_match = version.match(/[\d\.]+/)
|
||||||
|
|
||||||
if !version_match
|
raise ChromeError.new("Can't get the #{binary} version") if !version_match
|
||||||
raise ChromeError.new("Can't get the #{binary} version")
|
|
||||||
end
|
|
||||||
|
|
||||||
if Gem::Version.new(version_match[0]) < Gem::Version.new("59")
|
if Gem::Version.new(version_match[0]) < Gem::Version.new("59")
|
||||||
raise ChromeVersionTooLow.new("Chrome 59 or higher is required")
|
raise ChromeVersionTooLow.new("Chrome 59 or higher is required")
|
||||||
|
@ -28,24 +28,27 @@ class CommentMigration < ActiveRecord::Migration[4.2]
|
|||||||
end
|
end
|
||||||
|
|
||||||
def down
|
def down
|
||||||
replace_nils(comments_up).deep_merge(comments_down).each do |table|
|
replace_nils(comments_up)
|
||||||
table[1].each do |column|
|
.deep_merge(comments_down)
|
||||||
table_name = table[0]
|
.each do |table|
|
||||||
column_name = column[0]
|
table[1].each do |column|
|
||||||
comment = column[1]
|
table_name = table[0]
|
||||||
|
column_name = column[0]
|
||||||
|
comment = column[1]
|
||||||
|
|
||||||
if column_name == :_table
|
if column_name == :_table
|
||||||
DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment
|
DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment
|
||||||
puts " COMMENT ON TABLE #{table_name}"
|
puts " COMMENT ON TABLE #{table_name}"
|
||||||
else
|
else
|
||||||
DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment
|
DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment
|
||||||
puts " COMMENT ON COLUMN #{table_name}.#{column_name}"
|
puts " COMMENT ON COLUMN #{table_name}.#{column_name}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def replace_nils(hash)
|
def replace_nils(hash)
|
||||||
hash.each do |key, value|
|
hash.each do |key, value|
|
||||||
if Hash === value
|
if Hash === value
|
||||||
|
@ -12,9 +12,8 @@
|
|||||||
# Discourse.redis.without_namespace.del CommonPasswords::LIST_KEY
|
# Discourse.redis.without_namespace.del CommonPasswords::LIST_KEY
|
||||||
|
|
||||||
class CommonPasswords
|
class CommonPasswords
|
||||||
|
PASSWORD_FILE = File.join(Rails.root, "lib", "common_passwords", "10-char-common-passwords.txt")
|
||||||
PASSWORD_FILE = File.join(Rails.root, 'lib', 'common_passwords', '10-char-common-passwords.txt')
|
LIST_KEY = "discourse-common-passwords"
|
||||||
LIST_KEY = 'discourse-common-passwords'
|
|
||||||
|
|
||||||
@mutex = Mutex.new
|
@mutex = Mutex.new
|
||||||
|
|
||||||
@ -32,9 +31,7 @@ class CommonPasswords
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.password_list
|
def self.password_list
|
||||||
@mutex.synchronize do
|
@mutex.synchronize { load_passwords unless redis.scard(LIST_KEY) > 0 }
|
||||||
load_passwords unless redis.scard(LIST_KEY) > 0
|
|
||||||
end
|
|
||||||
RedisPasswordList.new
|
RedisPasswordList.new
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -49,5 +46,4 @@ class CommonPasswords
|
|||||||
# tolerate this so we don't block signups
|
# tolerate this so we don't block signups
|
||||||
Rails.logger.error "Common passwords file #{PASSWORD_FILE} is not found! Common password checking is skipped."
|
Rails.logger.error "Common passwords file #{PASSWORD_FILE} is not found! Common password checking is skipped."
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class ComposerMessagesFinder
|
class ComposerMessagesFinder
|
||||||
|
|
||||||
def initialize(user, details)
|
def initialize(user, details)
|
||||||
@user = user
|
@user = user
|
||||||
@details = details
|
@details = details
|
||||||
@ -29,26 +28,30 @@ class ComposerMessagesFinder
|
|||||||
|
|
||||||
if creating_topic?
|
if creating_topic?
|
||||||
count = @user.created_topic_count
|
count = @user.created_topic_count
|
||||||
education_key = 'education.new-topic'
|
education_key = "education.new-topic"
|
||||||
else
|
else
|
||||||
count = @user.post_count
|
count = @user.post_count
|
||||||
education_key = 'education.new-reply'
|
education_key = "education.new-reply"
|
||||||
end
|
end
|
||||||
|
|
||||||
if count < SiteSetting.educate_until_posts
|
if count < SiteSetting.educate_until_posts
|
||||||
return {
|
return(
|
||||||
id: 'education',
|
{
|
||||||
templateName: 'education',
|
id: "education",
|
||||||
wait_for_typing: true,
|
templateName: "education",
|
||||||
body: PrettyText.cook(
|
wait_for_typing: true,
|
||||||
I18n.t(
|
body:
|
||||||
education_key,
|
PrettyText.cook(
|
||||||
education_posts_text: I18n.t('education.until_posts', count: SiteSetting.educate_until_posts),
|
I18n.t(
|
||||||
site_name: SiteSetting.title,
|
education_key,
|
||||||
base_path: Discourse.base_path
|
education_posts_text:
|
||||||
)
|
I18n.t("education.until_posts", count: SiteSetting.educate_until_posts),
|
||||||
)
|
site_name: SiteSetting.title,
|
||||||
}
|
base_path: Discourse.base_path,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
@ -59,35 +62,55 @@ class ComposerMessagesFinder
|
|||||||
return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id])
|
return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id])
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'too_many_replies',
|
id: "too_many_replies",
|
||||||
templateName: 'education',
|
templateName: "education",
|
||||||
body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic))
|
body:
|
||||||
|
PrettyText.cook(
|
||||||
|
I18n.t(
|
||||||
|
"education.too_many_replies",
|
||||||
|
newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic,
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# Should a user be contacted to update their avatar?
|
# Should a user be contacted to update their avatar?
|
||||||
def check_avatar_notification
|
def check_avatar_notification
|
||||||
|
|
||||||
# A user has to be basic at least to be considered for an avatar notification
|
# A user has to be basic at least to be considered for an avatar notification
|
||||||
return unless @user.has_trust_level?(TrustLevel[1])
|
return unless @user.has_trust_level?(TrustLevel[1])
|
||||||
|
|
||||||
# We don't notify users who have avatars or who have been notified already.
|
# We don't notify users who have avatars or who have been notified already.
|
||||||
return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar)
|
if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# Do not notify user if any of the following is true:
|
# Do not notify user if any of the following is true:
|
||||||
# - "disable avatar education message" is enabled
|
# - "disable avatar education message" is enabled
|
||||||
# - "sso overrides avatar" is enabled
|
# - "sso overrides avatar" is enabled
|
||||||
# - "allow uploaded avatars" is disabled
|
# - "allow uploaded avatars" is disabled
|
||||||
return if SiteSetting.disable_avatar_education_message || SiteSetting.discourse_connect_overrides_avatar || !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user)
|
if SiteSetting.disable_avatar_education_message ||
|
||||||
|
SiteSetting.discourse_connect_overrides_avatar ||
|
||||||
|
!TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# If we got this far, log that we've nagged them about the avatar
|
# If we got this far, log that we've nagged them about the avatar
|
||||||
UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id)
|
UserHistory.create!(
|
||||||
|
action: UserHistory.actions[:notified_about_avatar],
|
||||||
|
target_user_id: @user.id,
|
||||||
|
)
|
||||||
|
|
||||||
# Return the message
|
# Return the message
|
||||||
{
|
{
|
||||||
id: 'avatar',
|
id: "avatar",
|
||||||
templateName: 'education',
|
templateName: "education",
|
||||||
body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture"))
|
body:
|
||||||
|
PrettyText.cook(
|
||||||
|
I18n.t(
|
||||||
|
"education.avatar",
|
||||||
|
profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture",
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -96,39 +119,45 @@ class ComposerMessagesFinder
|
|||||||
return unless educate_reply?(:notified_about_sequential_replies)
|
return unless educate_reply?(:notified_about_sequential_replies)
|
||||||
|
|
||||||
# Count the posts made by this user in the last day
|
# Count the posts made by this user in the last day
|
||||||
recent_posts_user_ids = Post.where(topic_id: @details[:topic_id])
|
recent_posts_user_ids =
|
||||||
.where("created_at > ?", 1.day.ago)
|
Post
|
||||||
.where(post_type: Post.types[:regular])
|
.where(topic_id: @details[:topic_id])
|
||||||
.order('created_at desc')
|
.where("created_at > ?", 1.day.ago)
|
||||||
.limit(SiteSetting.sequential_replies_threshold)
|
.where(post_type: Post.types[:regular])
|
||||||
.pluck(:user_id)
|
.order("created_at desc")
|
||||||
|
.limit(SiteSetting.sequential_replies_threshold)
|
||||||
|
.pluck(:user_id)
|
||||||
|
|
||||||
# Did we get back as many posts as we asked for, and are they all by the current user?
|
# Did we get back as many posts as we asked for, and are they all by the current user?
|
||||||
return if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold ||
|
if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold ||
|
||||||
recent_posts_user_ids.detect { |u| u != @user.id }
|
recent_posts_user_ids.detect { |u| u != @user.id }
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
# If we got this far, log that we've nagged them about the sequential replies
|
# If we got this far, log that we've nagged them about the sequential replies
|
||||||
UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies],
|
UserHistory.create!(
|
||||||
target_user_id: @user.id,
|
action: UserHistory.actions[:notified_about_sequential_replies],
|
||||||
topic_id: @details[:topic_id])
|
target_user_id: @user.id,
|
||||||
|
topic_id: @details[:topic_id],
|
||||||
|
)
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'sequential_replies',
|
id: "sequential_replies",
|
||||||
templateName: 'education',
|
templateName: "education",
|
||||||
wait_for_typing: true,
|
wait_for_typing: true,
|
||||||
extraClass: 'education-message',
|
extraClass: "education-message",
|
||||||
hide_if_whisper: true,
|
hide_if_whisper: true,
|
||||||
body: PrettyText.cook(I18n.t('education.sequential_replies'))
|
body: PrettyText.cook(I18n.t("education.sequential_replies")),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_dominating_topic
|
def check_dominating_topic
|
||||||
return unless educate_reply?(:notified_about_dominating_topic)
|
return unless educate_reply?(:notified_about_dominating_topic)
|
||||||
|
|
||||||
return if @topic.blank? ||
|
if @topic.blank? || @topic.user_id == @user.id ||
|
||||||
@topic.user_id == @user.id ||
|
@topic.posts_count < SiteSetting.summary_posts_required || @topic.private_message?
|
||||||
@topic.posts_count < SiteSetting.summary_posts_required ||
|
return
|
||||||
@topic.private_message?
|
end
|
||||||
|
|
||||||
posts_by_user = @user.posts.where(topic_id: @topic.id).count
|
posts_by_user = @user.posts.where(topic_id: @topic.id).count
|
||||||
|
|
||||||
@ -136,16 +165,18 @@ class ComposerMessagesFinder
|
|||||||
return if ratio < (SiteSetting.dominating_topic_minimum_percent.to_f / 100.0)
|
return if ratio < (SiteSetting.dominating_topic_minimum_percent.to_f / 100.0)
|
||||||
|
|
||||||
# Log the topic notification
|
# Log the topic notification
|
||||||
UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic],
|
UserHistory.create!(
|
||||||
target_user_id: @user.id,
|
action: UserHistory.actions[:notified_about_dominating_topic],
|
||||||
topic_id: @details[:topic_id])
|
target_user_id: @user.id,
|
||||||
|
topic_id: @details[:topic_id],
|
||||||
|
)
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'dominating_topic',
|
id: "dominating_topic",
|
||||||
templateName: 'dominating-topic',
|
templateName: "dominating-topic",
|
||||||
wait_for_typing: true,
|
wait_for_typing: true,
|
||||||
extraClass: 'education-message dominating-topic-message',
|
extraClass: "education-message dominating-topic-message",
|
||||||
body: PrettyText.cook(I18n.t('education.dominating_topic'))
|
body: PrettyText.cook(I18n.t("education.dominating_topic")),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -157,73 +188,85 @@ class ComposerMessagesFinder
|
|||||||
reply_to_user_id = Post.where(id: @details[:post_id]).pluck(:user_id)[0]
|
reply_to_user_id = Post.where(id: @details[:post_id]).pluck(:user_id)[0]
|
||||||
|
|
||||||
# Users's last x posts in the topic
|
# Users's last x posts in the topic
|
||||||
last_x_replies = @topic.
|
last_x_replies =
|
||||||
posts.
|
@topic
|
||||||
where(user_id: @user.id).
|
.posts
|
||||||
order('created_at desc').
|
.where(user_id: @user.id)
|
||||||
limit(SiteSetting.get_a_room_threshold).
|
.order("created_at desc")
|
||||||
pluck(:reply_to_user_id).
|
.limit(SiteSetting.get_a_room_threshold)
|
||||||
find_all { |uid| uid != @user.id && uid == reply_to_user_id }
|
.pluck(:reply_to_user_id)
|
||||||
|
.find_all { |uid| uid != @user.id && uid == reply_to_user_id }
|
||||||
|
|
||||||
return unless last_x_replies.size == SiteSetting.get_a_room_threshold
|
return unless last_x_replies.size == SiteSetting.get_a_room_threshold
|
||||||
return unless @topic.posts.count('distinct user_id') >= min_users_posted
|
return unless @topic.posts.count("distinct user_id") >= min_users_posted
|
||||||
|
|
||||||
UserHistory.create!(action: UserHistory.actions[:notified_about_get_a_room],
|
UserHistory.create!(
|
||||||
target_user_id: @user.id,
|
action: UserHistory.actions[:notified_about_get_a_room],
|
||||||
topic_id: @details[:topic_id])
|
target_user_id: @user.id,
|
||||||
|
topic_id: @details[:topic_id],
|
||||||
|
)
|
||||||
|
|
||||||
reply_username = User.where(id: last_x_replies[0]).pluck_first(:username)
|
reply_username = User.where(id: last_x_replies[0]).pluck_first(:username)
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'get_a_room',
|
id: "get_a_room",
|
||||||
templateName: 'get-a-room',
|
templateName: "get-a-room",
|
||||||
wait_for_typing: true,
|
wait_for_typing: true,
|
||||||
reply_username: reply_username,
|
reply_username: reply_username,
|
||||||
extraClass: 'education-message get-a-room',
|
extraClass: "education-message get-a-room",
|
||||||
body: PrettyText.cook(
|
body:
|
||||||
I18n.t(
|
PrettyText.cook(
|
||||||
'education.get_a_room',
|
I18n.t(
|
||||||
count: SiteSetting.get_a_room_threshold,
|
"education.get_a_room",
|
||||||
reply_username: reply_username,
|
count: SiteSetting.get_a_room_threshold,
|
||||||
base_path: Discourse.base_path
|
reply_username: reply_username,
|
||||||
)
|
base_path: Discourse.base_path,
|
||||||
)
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def check_reviving_old_topic
|
def check_reviving_old_topic
|
||||||
return unless replying?
|
return unless replying?
|
||||||
return if @topic.nil? ||
|
if @topic.nil? || SiteSetting.warn_reviving_old_topic_age < 1 || @topic.last_posted_at.nil? ||
|
||||||
SiteSetting.warn_reviving_old_topic_age < 1 ||
|
@topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago
|
||||||
@topic.last_posted_at.nil? ||
|
return
|
||||||
@topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
id: 'reviving_old',
|
id: "reviving_old",
|
||||||
templateName: 'education',
|
templateName: "education",
|
||||||
wait_for_typing: false,
|
wait_for_typing: false,
|
||||||
extraClass: 'education-message',
|
extraClass: "education-message",
|
||||||
body: PrettyText.cook(
|
body:
|
||||||
I18n.t(
|
PrettyText.cook(
|
||||||
'education.reviving_old_topic',
|
I18n.t(
|
||||||
time_ago: FreedomPatches::Rails4.time_ago_in_words(@topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose')
|
"education.reviving_old_topic",
|
||||||
)
|
time_ago:
|
||||||
)
|
FreedomPatches::Rails4.time_ago_in_words(
|
||||||
|
@topic.last_posted_at,
|
||||||
|
false,
|
||||||
|
scope: :"datetime.distance_in_words_verbose",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.user_not_seen_in_a_while(usernames)
|
def self.user_not_seen_in_a_while(usernames)
|
||||||
User.where(username_lower: usernames).where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago).pluck(:username).sort
|
User
|
||||||
|
.where(username_lower: usernames)
|
||||||
|
.where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago)
|
||||||
|
.pluck(:username)
|
||||||
|
.sort
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def educate_reply?(type)
|
def educate_reply?(type)
|
||||||
replying? &&
|
replying? && @details[:topic_id] && (@topic.present? && !@topic.private_message?) &&
|
||||||
@details[:topic_id] &&
|
(@user.post_count >= SiteSetting.educate_until_posts) &&
|
||||||
(@topic.present? && !@topic.private_message?) &&
|
!UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id])
|
||||||
(@user.post_count >= SiteSetting.educate_until_posts) &&
|
|
||||||
!UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def creating_topic?
|
def creating_topic?
|
||||||
@ -237,5 +280,4 @@ class ComposerMessagesFinder
|
|||||||
def editing_post?
|
def editing_post?
|
||||||
@details[:composer_action] == "edit"
|
@details[:composer_action] == "edit"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -9,12 +9,13 @@ module Compression
|
|||||||
Compression::Zip.new,
|
Compression::Zip.new,
|
||||||
Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]),
|
Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]),
|
||||||
Compression::Gzip.new,
|
Compression::Gzip.new,
|
||||||
Compression::Tar.new
|
Compression::Tar.new,
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.engine_for(filename, strategies: default_strategies)
|
def self.engine_for(filename, strategies: default_strategies)
|
||||||
strategy = strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) }
|
strategy =
|
||||||
|
strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) }
|
||||||
new(strategy)
|
new(strategy)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,12 +3,17 @@
|
|||||||
module Compression
|
module Compression
|
||||||
class Gzip < Strategy
|
class Gzip < Strategy
|
||||||
def extension
|
def extension
|
||||||
'.gz'
|
".gz"
|
||||||
end
|
end
|
||||||
|
|
||||||
def compress(path, target_name)
|
def compress(path, target_name)
|
||||||
gzip_target = sanitize_path("#{path}/#{target_name}")
|
gzip_target = sanitize_path("#{path}/#{target_name}")
|
||||||
Discourse::Utils.execute_command('gzip', '-5', gzip_target, failure_message: "Failed to gzip file.")
|
Discourse::Utils.execute_command(
|
||||||
|
"gzip",
|
||||||
|
"-5",
|
||||||
|
gzip_target,
|
||||||
|
failure_message: "Failed to gzip file.",
|
||||||
|
)
|
||||||
|
|
||||||
"#{gzip_target}.gz"
|
"#{gzip_target}.gz"
|
||||||
end
|
end
|
||||||
@ -23,7 +28,8 @@ module Compression
|
|||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_folder(_entry, _entry_path); end
|
def extract_folder(_entry, _entry_path)
|
||||||
|
end
|
||||||
|
|
||||||
def get_compressed_file_stream(compressed_file_path)
|
def get_compressed_file_stream(compressed_file_path)
|
||||||
gzip = Zlib::GzipReader.open(compressed_file_path)
|
gzip = Zlib::GzipReader.open(compressed_file_path)
|
||||||
@ -32,7 +38,7 @@ module Compression
|
|||||||
|
|
||||||
def build_entry_path(dest_path, _, compressed_file_path)
|
def build_entry_path(dest_path, _, compressed_file_path)
|
||||||
basename = File.basename(compressed_file_path)
|
basename = File.basename(compressed_file_path)
|
||||||
basename.gsub!(/#{Regexp.escape(extension)}$/, '')
|
basename.gsub!(/#{Regexp.escape(extension)}$/, "")
|
||||||
File.join(dest_path, basename)
|
File.join(dest_path, basename)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -44,12 +50,11 @@ module Compression
|
|||||||
remaining_size = available_size
|
remaining_size = available_size
|
||||||
|
|
||||||
if ::File.exist?(entry_path)
|
if ::File.exist?(entry_path)
|
||||||
raise ::Zip::DestinationFileExistsError,
|
raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists"
|
||||||
"Destination '#{entry_path}' already exists"
|
|
||||||
end # Change this later.
|
end # Change this later.
|
||||||
|
|
||||||
::File.open(entry_path, 'wb') do |os|
|
::File.open(entry_path, "wb") do |os|
|
||||||
buf = ''.dup
|
buf = "".dup
|
||||||
while (buf = entry.read(chunk_size))
|
while (buf = entry.read(chunk_size))
|
||||||
remaining_size -= chunk_size
|
remaining_size -= chunk_size
|
||||||
raise ExtractFailed if remaining_size.negative?
|
raise ExtractFailed if remaining_size.negative?
|
||||||
|
@ -7,25 +7,27 @@ module Compression
|
|||||||
end
|
end
|
||||||
|
|
||||||
def extension
|
def extension
|
||||||
@strategies.reduce('') { |ext, strategy| ext += strategy.extension }
|
@strategies.reduce("") { |ext, strategy| ext += strategy.extension }
|
||||||
end
|
end
|
||||||
|
|
||||||
def compress(path, target_name)
|
def compress(path, target_name)
|
||||||
current_target = target_name
|
current_target = target_name
|
||||||
@strategies.reduce('') do |compressed_path, strategy|
|
@strategies.reduce("") do |compressed_path, strategy|
|
||||||
compressed_path = strategy.compress(path, current_target)
|
compressed_path = strategy.compress(path, current_target)
|
||||||
current_target = compressed_path.split('/').last
|
current_target = compressed_path.split("/").last
|
||||||
|
|
||||||
compressed_path
|
compressed_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def decompress(dest_path, compressed_file_path, max_size)
|
def decompress(dest_path, compressed_file_path, max_size)
|
||||||
@strategies.reverse.reduce(compressed_file_path) do |to_decompress, strategy|
|
@strategies
|
||||||
next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size)
|
.reverse
|
||||||
FileUtils.rm_rf(to_decompress)
|
.reduce(compressed_file_path) do |to_decompress, strategy|
|
||||||
next_compressed_file
|
next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size)
|
||||||
end
|
FileUtils.rm_rf(to_decompress)
|
||||||
|
next_compressed_file
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -18,9 +18,7 @@ module Compression
|
|||||||
|
|
||||||
entries_of(compressed_file).each do |entry|
|
entries_of(compressed_file).each do |entry|
|
||||||
entry_path = build_entry_path(sanitized_dest_path, entry, sanitized_compressed_file_path)
|
entry_path = build_entry_path(sanitized_dest_path, entry, sanitized_compressed_file_path)
|
||||||
if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path)
|
next if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path)
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
FileUtils.mkdir_p(File.dirname(entry_path))
|
FileUtils.mkdir_p(File.dirname(entry_path))
|
||||||
if is_file?(entry)
|
if is_file?(entry)
|
||||||
@ -45,10 +43,10 @@ module Compression
|
|||||||
filename.strip.tap do |name|
|
filename.strip.tap do |name|
|
||||||
# NOTE: File.basename doesn't work right with Windows paths on Unix
|
# NOTE: File.basename doesn't work right with Windows paths on Unix
|
||||||
# get only the filename, not the whole path
|
# get only the filename, not the whole path
|
||||||
name.sub! /\A.*(\\|\/)/, ''
|
name.sub! %r{\A.*(\\|/)}, ""
|
||||||
# Finally, replace all non alphanumeric, underscore
|
# Finally, replace all non alphanumeric, underscore
|
||||||
# or periods with underscore
|
# or periods with underscore
|
||||||
name.gsub! /[^\w\.\-]/, '_'
|
name.gsub! /[^\w\.\-]/, "_"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -75,7 +73,7 @@ module Compression
|
|||||||
raise DestinationFileExistsError, "Destination '#{entry_path}' already exists"
|
raise DestinationFileExistsError, "Destination '#{entry_path}' already exists"
|
||||||
end
|
end
|
||||||
|
|
||||||
::File.open(entry_path, 'wb') do |os|
|
::File.open(entry_path, "wb") do |os|
|
||||||
while (buf = entry.read(chunk_size))
|
while (buf = entry.read(chunk_size))
|
||||||
remaining_size -= buf.size
|
remaining_size -= buf.size
|
||||||
raise ExtractFailed if remaining_size.negative?
|
raise ExtractFailed if remaining_size.negative?
|
||||||
|
@ -1,23 +1,31 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rubygems/package'
|
require "rubygems/package"
|
||||||
|
|
||||||
module Compression
|
module Compression
|
||||||
class Tar < Strategy
|
class Tar < Strategy
|
||||||
def extension
|
def extension
|
||||||
'.tar'
|
".tar"
|
||||||
end
|
end
|
||||||
|
|
||||||
def compress(path, target_name)
|
def compress(path, target_name)
|
||||||
tar_filename = sanitize_filename("#{target_name}.tar")
|
tar_filename = sanitize_filename("#{target_name}.tar")
|
||||||
Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, target_name, failure_message: "Failed to tar file.")
|
Discourse::Utils.execute_command(
|
||||||
|
"tar",
|
||||||
|
"--create",
|
||||||
|
"--file",
|
||||||
|
tar_filename,
|
||||||
|
target_name,
|
||||||
|
failure_message: "Failed to tar file.",
|
||||||
|
)
|
||||||
|
|
||||||
sanitize_path("#{path}/#{tar_filename}")
|
sanitize_path("#{path}/#{tar_filename}")
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def extract_folder(_entry, _entry_path); end
|
def extract_folder(_entry, _entry_path)
|
||||||
|
end
|
||||||
|
|
||||||
def get_compressed_file_stream(compressed_file_path)
|
def get_compressed_file_stream(compressed_file_path)
|
||||||
file_stream = IO.new(IO.sysopen(compressed_file_path))
|
file_stream = IO.new(IO.sysopen(compressed_file_path))
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'zip'
|
require "zip"
|
||||||
|
|
||||||
module Compression
|
module Compression
|
||||||
class Zip < Strategy
|
class Zip < Strategy
|
||||||
def extension
|
def extension
|
||||||
'.zip'
|
".zip"
|
||||||
end
|
end
|
||||||
|
|
||||||
def compress(path, target_name)
|
def compress(path, target_name)
|
||||||
@ -15,7 +15,7 @@ module Compression
|
|||||||
::Zip::File.open(zip_filename, ::Zip::File::CREATE) do |zipfile|
|
::Zip::File.open(zip_filename, ::Zip::File::CREATE) do |zipfile|
|
||||||
if File.directory?(absolute_path)
|
if File.directory?(absolute_path)
|
||||||
entries = Dir.entries(absolute_path) - %w[. ..]
|
entries = Dir.entries(absolute_path) - %w[. ..]
|
||||||
write_entries(entries, absolute_path, '', zipfile)
|
write_entries(entries, absolute_path, "", zipfile)
|
||||||
else
|
else
|
||||||
put_into_archive(absolute_path, zipfile, target_name)
|
put_into_archive(absolute_path, zipfile, target_name)
|
||||||
end
|
end
|
||||||
@ -47,15 +47,14 @@ module Compression
|
|||||||
remaining_size = available_size
|
remaining_size = available_size
|
||||||
|
|
||||||
if ::File.exist?(entry_path)
|
if ::File.exist?(entry_path)
|
||||||
raise ::Zip::DestinationFileExistsError,
|
raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists"
|
||||||
"Destination '#{entry_path}' already exists"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
::File.open(entry_path, 'wb') do |os|
|
::File.open(entry_path, "wb") do |os|
|
||||||
entry.get_input_stream do |is|
|
entry.get_input_stream do |is|
|
||||||
entry.set_extra_attributes_on_path(entry_path)
|
entry.set_extra_attributes_on_path(entry_path)
|
||||||
|
|
||||||
buf = ''.dup
|
buf = "".dup
|
||||||
while (buf = is.sysread(chunk_size, buf))
|
while (buf = is.sysread(chunk_size, buf))
|
||||||
remaining_size -= chunk_size
|
remaining_size -= chunk_size
|
||||||
raise ExtractFailed if remaining_size.negative?
|
raise ExtractFailed if remaining_size.negative?
|
||||||
@ -70,7 +69,7 @@ module Compression
|
|||||||
# A helper method to make the recursion work.
|
# A helper method to make the recursion work.
|
||||||
def write_entries(entries, base_path, path, zipfile)
|
def write_entries(entries, base_path, path, zipfile)
|
||||||
entries.each do |e|
|
entries.each do |e|
|
||||||
zipfile_path = path == '' ? e : File.join(path, e)
|
zipfile_path = path == "" ? e : File.join(path, e)
|
||||||
disk_file_path = File.join(base_path, zipfile_path)
|
disk_file_path = File.join(base_path, zipfile_path)
|
||||||
|
|
||||||
if File.directory? disk_file_path
|
if File.directory? disk_file_path
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ConfigurableUrls
|
module ConfigurableUrls
|
||||||
|
|
||||||
def faq_path
|
def faq_path
|
||||||
SiteSetting.faq_url.blank? ? "#{Discourse.base_path}/faq" : SiteSetting.faq_url
|
SiteSetting.faq_url.blank? ? "#{Discourse.base_path}/faq" : SiteSetting.faq_url
|
||||||
end
|
end
|
||||||
@ -11,7 +10,10 @@ module ConfigurableUrls
|
|||||||
end
|
end
|
||||||
|
|
||||||
def privacy_path
|
def privacy_path
|
||||||
SiteSetting.privacy_policy_url.blank? ? "#{Discourse.base_path}/privacy" : SiteSetting.privacy_policy_url
|
if SiteSetting.privacy_policy_url.blank?
|
||||||
|
"#{Discourse.base_path}/privacy"
|
||||||
|
else
|
||||||
|
SiteSetting.privacy_policy_url
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
# this class is used to track changes to an arbitrary buffer
|
# this class is used to track changes to an arbitrary buffer
|
||||||
|
|
||||||
class ContentBuffer
|
class ContentBuffer
|
||||||
|
|
||||||
def initialize(initial_content)
|
def initialize(initial_content)
|
||||||
@initial_content = initial_content
|
@initial_content = initial_content
|
||||||
@lines = @initial_content.split("\n")
|
@lines = @initial_content.split("\n")
|
||||||
@ -17,7 +16,6 @@ class ContentBuffer
|
|||||||
text = transform[:text]
|
text = transform[:text]
|
||||||
|
|
||||||
if transform[:operation] == :delete
|
if transform[:operation] == :delete
|
||||||
|
|
||||||
# fix first line
|
# fix first line
|
||||||
|
|
||||||
l = @lines[start_row]
|
l = @lines[start_row]
|
||||||
@ -32,16 +30,13 @@ class ContentBuffer
|
|||||||
@lines[start_row] = l
|
@lines[start_row] = l
|
||||||
|
|
||||||
# remove middle lines
|
# remove middle lines
|
||||||
(finish_row - start_row).times do
|
(finish_row - start_row).times { l = @lines.delete_at start_row + 1 }
|
||||||
l = @lines.delete_at start_row + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
# fix last line
|
# fix last line
|
||||||
@lines[start_row] << @lines[finish_row][finish_col - 1..-1]
|
@lines[start_row] << @lines[finish_row][finish_col - 1..-1]
|
||||||
end
|
end
|
||||||
|
|
||||||
if transform[:operation] == :insert
|
if transform[:operation] == :insert
|
||||||
|
|
||||||
@lines[start_row].insert(start_col, text)
|
@lines[start_row].insert(start_col, text)
|
||||||
|
|
||||||
split = @lines[start_row].split("\n")
|
split = @lines[start_row].split("\n")
|
||||||
@ -56,7 +51,6 @@ class ContentBuffer
|
|||||||
@lines.insert(i, "") unless @lines.length > i
|
@lines.insert(i, "") unless @lines.length > i
|
||||||
@lines[i] = split[-1] + @lines[i]
|
@lines[i] = split[-1] + @lines[i]
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'content_security_policy/builder'
|
require "content_security_policy/builder"
|
||||||
require 'content_security_policy/extension'
|
require "content_security_policy/extension"
|
||||||
|
|
||||||
class ContentSecurityPolicy
|
class ContentSecurityPolicy
|
||||||
class << self
|
class << self
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'content_security_policy/default'
|
require "content_security_policy/default"
|
||||||
|
|
||||||
class ContentSecurityPolicy
|
class ContentSecurityPolicy
|
||||||
class Builder
|
class Builder
|
||||||
@ -33,7 +33,9 @@ class ContentSecurityPolicy
|
|||||||
def <<(extension)
|
def <<(extension)
|
||||||
return unless valid_extension?(extension)
|
return unless valid_extension?(extension)
|
||||||
|
|
||||||
extension.each { |directive, sources| extend_directive(normalize_directive(directive), sources) }
|
extension.each do |directive, sources|
|
||||||
|
extend_directive(normalize_directive(directive), sources)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def build
|
def build
|
||||||
@ -53,7 +55,7 @@ class ContentSecurityPolicy
|
|||||||
private
|
private
|
||||||
|
|
||||||
def normalize_directive(directive)
|
def normalize_directive(directive)
|
||||||
directive.to_s.gsub('-', '_').to_sym
|
directive.to_s.gsub("-", "_").to_sym
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_source(source)
|
def normalize_source(source)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'content_security_policy'
|
require "content_security_policy"
|
||||||
|
|
||||||
class ContentSecurityPolicy
|
class ContentSecurityPolicy
|
||||||
class Default
|
class Default
|
||||||
@ -7,16 +7,19 @@ class ContentSecurityPolicy
|
|||||||
|
|
||||||
def initialize(base_url:)
|
def initialize(base_url:)
|
||||||
@base_url = base_url
|
@base_url = base_url
|
||||||
@directives = {}.tap do |directives|
|
@directives =
|
||||||
directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https
|
{}.tap do |directives|
|
||||||
directives[:base_uri] = [:self]
|
directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https
|
||||||
directives[:object_src] = [:none]
|
directives[:base_uri] = [:self]
|
||||||
directives[:script_src] = script_src
|
directives[:object_src] = [:none]
|
||||||
directives[:worker_src] = worker_src
|
directives[:script_src] = script_src
|
||||||
directives[:report_uri] = report_uri if SiteSetting.content_security_policy_collect_reports
|
directives[:worker_src] = worker_src
|
||||||
directives[:frame_ancestors] = frame_ancestors if restrict_embed?
|
directives[
|
||||||
directives[:manifest_src] = ["'self'"]
|
:report_uri
|
||||||
end
|
] = report_uri if SiteSetting.content_security_policy_collect_reports
|
||||||
|
directives[:frame_ancestors] = frame_ancestors if restrict_embed?
|
||||||
|
directives[:manifest_src] = ["'self'"]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -27,27 +30,34 @@ class ContentSecurityPolicy
|
|||||||
|
|
||||||
SCRIPT_ASSET_DIRECTORIES = [
|
SCRIPT_ASSET_DIRECTORIES = [
|
||||||
# [dir, can_use_s3_cdn, can_use_cdn, for_worker]
|
# [dir, can_use_s3_cdn, can_use_cdn, for_worker]
|
||||||
['/assets/', true, true, true],
|
["/assets/", true, true, true],
|
||||||
['/brotli_asset/', true, true, true],
|
["/brotli_asset/", true, true, true],
|
||||||
['/extra-locales/', false, false, false],
|
["/extra-locales/", false, false, false],
|
||||||
['/highlight-js/', false, true, false],
|
["/highlight-js/", false, true, false],
|
||||||
['/javascripts/', false, true, true],
|
["/javascripts/", false, true, true],
|
||||||
['/plugins/', false, true, true],
|
["/plugins/", false, true, true],
|
||||||
['/theme-javascripts/', false, true, false],
|
["/theme-javascripts/", false, true, false],
|
||||||
['/svg-sprite/', false, true, false],
|
["/svg-sprite/", false, true, false],
|
||||||
]
|
]
|
||||||
|
|
||||||
def script_assets(base = base_url, s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, cdn = GlobalSetting.cdn_url, worker: false)
|
def script_assets(
|
||||||
SCRIPT_ASSET_DIRECTORIES.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker|
|
base = base_url,
|
||||||
next if worker && !for_worker
|
s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url,
|
||||||
if can_use_s3_cdn && s3_cdn
|
cdn = GlobalSetting.cdn_url,
|
||||||
s3_cdn + dir
|
worker: false
|
||||||
elsif can_use_cdn && cdn
|
)
|
||||||
cdn + Discourse.base_path + dir
|
SCRIPT_ASSET_DIRECTORIES
|
||||||
else
|
.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker|
|
||||||
base + dir
|
next if worker && !for_worker
|
||||||
|
if can_use_s3_cdn && s3_cdn
|
||||||
|
s3_cdn + dir
|
||||||
|
elsif can_use_cdn && cdn
|
||||||
|
cdn + Discourse.base_path + dir
|
||||||
|
else
|
||||||
|
base + dir
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end.compact
|
.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def script_src
|
def script_src
|
||||||
@ -55,7 +65,7 @@ class ContentSecurityPolicy
|
|||||||
"#{base_url}/logs/",
|
"#{base_url}/logs/",
|
||||||
"#{base_url}/sidekiq/",
|
"#{base_url}/sidekiq/",
|
||||||
"#{base_url}/mini-profiler-resources/",
|
"#{base_url}/mini-profiler-resources/",
|
||||||
*script_assets
|
*script_assets,
|
||||||
].tap do |sources|
|
].tap do |sources|
|
||||||
sources << :report_sample if SiteSetting.content_security_policy_collect_reports
|
sources << :report_sample if SiteSetting.content_security_policy_collect_reports
|
||||||
sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev
|
sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev
|
||||||
@ -67,23 +77,25 @@ class ContentSecurityPolicy
|
|||||||
end
|
end
|
||||||
|
|
||||||
# we need analytics.js still as gtag/js is a script wrapper for it
|
# we need analytics.js still as gtag/js is a script wrapper for it
|
||||||
sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present?
|
if SiteSetting.ga_universal_tracking_code.present?
|
||||||
sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag"
|
sources << "https://www.google-analytics.com/analytics.js"
|
||||||
|
end
|
||||||
|
if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag"
|
||||||
|
sources << "https://www.googletagmanager.com/gtag/js"
|
||||||
|
end
|
||||||
if SiteSetting.gtm_container_id.present?
|
if SiteSetting.gtm_container_id.present?
|
||||||
sources << 'https://www.googletagmanager.com/gtm.js'
|
sources << "https://www.googletagmanager.com/gtm.js"
|
||||||
sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'"
|
sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'"
|
||||||
end
|
end
|
||||||
|
|
||||||
if SiteSetting.splash_screen
|
sources << "'#{SplashScreenHelper.fingerprint}'" if SiteSetting.splash_screen
|
||||||
sources << "'#{SplashScreenHelper.fingerprint}'"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def worker_src
|
def worker_src
|
||||||
[
|
[
|
||||||
"'self'", # For service worker
|
"'self'", # For service worker
|
||||||
*script_assets(worker: true)
|
*script_assets(worker: true),
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -92,15 +104,11 @@ class ContentSecurityPolicy
|
|||||||
end
|
end
|
||||||
|
|
||||||
def frame_ancestors
|
def frame_ancestors
|
||||||
[
|
["'self'", *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }]
|
||||||
"'self'",
|
|
||||||
*EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }
|
|
||||||
]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def restrict_embed?
|
def restrict_embed?
|
||||||
SiteSetting.content_security_policy_frame_ancestors &&
|
SiteSetting.content_security_policy_frame_ancestors && !SiteSetting.embed_any_origin
|
||||||
!SiteSetting.embed_any_origin
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,12 +4,12 @@ class ContentSecurityPolicy
|
|||||||
extend self
|
extend self
|
||||||
|
|
||||||
def site_setting_extension
|
def site_setting_extension
|
||||||
{ script_src: SiteSetting.content_security_policy_script_src.split('|') }
|
{ script_src: SiteSetting.content_security_policy_script_src.split("|") }
|
||||||
end
|
end
|
||||||
|
|
||||||
def path_specific_extension(path_info)
|
def path_specific_extension(path_info)
|
||||||
{}.tap do |obj|
|
{}.tap do |obj|
|
||||||
for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info)
|
for_qunit_route = !Rails.env.production? && %w[/qunit /wizard/qunit].include?(path_info)
|
||||||
for_qunit_route ||= "/theme-qunit" == path_info
|
for_qunit_route ||= "/theme-qunit" == path_info
|
||||||
obj[:script_src] = :unsafe_eval if for_qunit_route
|
obj[:script_src] = :unsafe_eval if for_qunit_route
|
||||||
end
|
end
|
||||||
@ -23,7 +23,7 @@ class ContentSecurityPolicy
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
THEME_SETTING = 'extend_content_security_policy'
|
THEME_SETTING = "extend_content_security_policy"
|
||||||
|
|
||||||
def theme_extensions(theme_id)
|
def theme_extensions(theme_id)
|
||||||
key = "theme_extensions_#{theme_id}"
|
key = "theme_extensions_#{theme_id}"
|
||||||
@ -37,47 +37,55 @@ class ContentSecurityPolicy
|
|||||||
private
|
private
|
||||||
|
|
||||||
def cache
|
def cache
|
||||||
@cache ||= DistributedCache.new('csp_extensions')
|
@cache ||= DistributedCache.new("csp_extensions")
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_theme_extensions(theme_id)
|
def find_theme_extensions(theme_id)
|
||||||
extensions = []
|
extensions = []
|
||||||
theme_ids = Theme.transform_ids(theme_id)
|
theme_ids = Theme.transform_ids(theme_id)
|
||||||
|
|
||||||
Theme.where(id: theme_ids).find_each do |theme|
|
Theme
|
||||||
theme.cached_settings.each do |setting, value|
|
.where(id: theme_ids)
|
||||||
extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
|
.find_each do |theme|
|
||||||
|
theme.cached_settings.each do |setting, value|
|
||||||
|
extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions)
|
extensions << build_theme_extension(
|
||||||
|
ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions,
|
||||||
html_fields = ThemeField.where(
|
|
||||||
theme_id: theme_ids,
|
|
||||||
target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
|
|
||||||
name: ThemeField.html_fields
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
html_fields =
|
||||||
|
ThemeField.where(
|
||||||
|
theme_id: theme_ids,
|
||||||
|
target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] },
|
||||||
|
name: ThemeField.html_fields,
|
||||||
|
)
|
||||||
|
|
||||||
auto_script_src_extension = { script_src: [] }
|
auto_script_src_extension = { script_src: [] }
|
||||||
html_fields.each(&:ensure_baked!)
|
html_fields.each(&:ensure_baked!)
|
||||||
doc = html_fields.map(&:value_baked).join("\n")
|
doc = html_fields.map(&:value_baked).join("\n")
|
||||||
|
|
||||||
Nokogiri::HTML5.fragment(doc).css('script[src]').each do |node|
|
Nokogiri::HTML5
|
||||||
src = node['src']
|
.fragment(doc)
|
||||||
uri = URI(src)
|
.css("script[src]")
|
||||||
|
.each do |node|
|
||||||
|
src = node["src"]
|
||||||
|
uri = URI(src)
|
||||||
|
|
||||||
next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts)
|
next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts)
|
||||||
next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts)
|
next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts)
|
||||||
next if uri.path.nil? # Ignore raw hosts
|
next if uri.path.nil? # Ignore raw hosts
|
||||||
|
|
||||||
uri.query = nil # CSP should not include query part of url
|
uri.query = nil # CSP should not include query part of url
|
||||||
|
|
||||||
uri_string = uri.to_s.sub(/^\/\//, '') # Protocol-less CSP should not have // at beginning of URL
|
uri_string = uri.to_s.sub(%r{^//}, "") # Protocol-less CSP should not have // at beginning of URL
|
||||||
|
|
||||||
auto_script_src_extension[:script_src] << uri_string
|
auto_script_src_extension[:script_src] << uri_string
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
# Ignore invalid URI
|
# Ignore invalid URI
|
||||||
end
|
end
|
||||||
|
|
||||||
extensions << auto_script_src_extension
|
extensions << auto_script_src_extension
|
||||||
|
|
||||||
@ -87,7 +95,7 @@ class ContentSecurityPolicy
|
|||||||
def build_theme_extension(entries)
|
def build_theme_extension(entries)
|
||||||
{}.tap do |extension|
|
{}.tap do |extension|
|
||||||
entries.each do |entry|
|
entries.each do |entry|
|
||||||
directive, source = entry.split(':', 2).map(&:strip)
|
directive, source = entry.split(":", 2).map(&:strip)
|
||||||
|
|
||||||
extension[directive] ||= []
|
extension[directive] ||= []
|
||||||
extension[directive] << source
|
extension[directive] << source
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'content_security_policy'
|
require "content_security_policy"
|
||||||
|
|
||||||
class ContentSecurityPolicy
|
class ContentSecurityPolicy
|
||||||
class Middleware
|
class Middleware
|
||||||
@ -19,8 +19,16 @@ class ContentSecurityPolicy
|
|||||||
|
|
||||||
theme_id = env[:resolved_theme_id]
|
theme_id = env[:resolved_theme_id]
|
||||||
|
|
||||||
headers['Content-Security-Policy'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy
|
headers["Content-Security-Policy"] = policy(
|
||||||
headers['Content-Security-Policy-Report-Only'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only
|
theme_id,
|
||||||
|
base_url: base_url,
|
||||||
|
path_info: env["PATH_INFO"],
|
||||||
|
) if SiteSetting.content_security_policy
|
||||||
|
headers["Content-Security-Policy-Report-Only"] = policy(
|
||||||
|
theme_id,
|
||||||
|
base_url: base_url,
|
||||||
|
path_info: env["PATH_INFO"],
|
||||||
|
) if SiteSetting.content_security_policy_report_only
|
||||||
|
|
||||||
response
|
response
|
||||||
end
|
end
|
||||||
@ -30,7 +38,7 @@ class ContentSecurityPolicy
|
|||||||
delegate :policy, to: :ContentSecurityPolicy
|
delegate :policy, to: :ContentSecurityPolicy
|
||||||
|
|
||||||
def html_response?(headers)
|
def html_response?(headers)
|
||||||
headers['Content-Type'] && headers['Content-Type'] =~ /html/
|
headers["Content-Type"] && headers["Content-Type"] =~ /html/
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,7 +7,7 @@ class CookedPostProcessor
|
|||||||
include CookedProcessorMixin
|
include CookedProcessorMixin
|
||||||
|
|
||||||
LIGHTBOX_WRAPPER_CSS_CLASS = "lightbox-wrapper"
|
LIGHTBOX_WRAPPER_CSS_CLASS = "lightbox-wrapper"
|
||||||
GIF_SOURCES_REGEXP = /(giphy|tenor)\.com\//
|
GIF_SOURCES_REGEXP = %r{(giphy|tenor)\.com/}
|
||||||
|
|
||||||
attr_reader :cooking_options, :doc
|
attr_reader :cooking_options, :doc
|
||||||
|
|
||||||
@ -61,25 +61,27 @@ class CookedPostProcessor
|
|||||||
return if @post.user.blank? || !Guardian.new.can_see?(@post)
|
return if @post.user.blank? || !Guardian.new.can_see?(@post)
|
||||||
|
|
||||||
BadgeGranter.grant(Badge.find(Badge::FirstEmoji), @post.user, post_id: @post.id) if has_emoji?
|
BadgeGranter.grant(Badge.find(Badge::FirstEmoji), @post.user, post_id: @post.id) if has_emoji?
|
||||||
BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) if @has_oneboxes
|
if @has_oneboxes
|
||||||
BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) if @post.is_reply_by_email?
|
BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id)
|
||||||
|
end
|
||||||
|
if @post.is_reply_by_email?
|
||||||
|
BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def post_process_quotes
|
def post_process_quotes
|
||||||
@doc.css("aside.quote").each do |q|
|
@doc
|
||||||
post_number = q['data-post']
|
.css("aside.quote")
|
||||||
topic_id = q['data-topic']
|
.each do |q|
|
||||||
if topic_id && post_number
|
post_number = q["data-post"]
|
||||||
comparer = QuoteComparer.new(
|
topic_id = q["data-topic"]
|
||||||
topic_id.to_i,
|
if topic_id && post_number
|
||||||
post_number.to_i,
|
comparer = QuoteComparer.new(topic_id.to_i, post_number.to_i, q.css("blockquote").text)
|
||||||
q.css('blockquote').text
|
|
||||||
)
|
|
||||||
|
|
||||||
q['class'] = ((q['class'] || '') + " quote-post-not-found").strip if comparer.missing?
|
q["class"] = ((q["class"] || "") + " quote-post-not-found").strip if comparer.missing?
|
||||||
q['class'] = ((q['class'] || '') + " quote-modified").strip if comparer.modified?
|
q["class"] = ((q["class"] || "") + " quote-modified").strip if comparer.modified?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_full_quote_on_direct_reply
|
def remove_full_quote_on_direct_reply
|
||||||
@ -87,66 +89,68 @@ class CookedPostProcessor
|
|||||||
return if @post.post_number == 1
|
return if @post.post_number == 1
|
||||||
return if @doc.xpath("aside[contains(@class, 'quote')]").size != 1
|
return if @doc.xpath("aside[contains(@class, 'quote')]").size != 1
|
||||||
|
|
||||||
previous = Post
|
previous =
|
||||||
.where("post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", @post.post_number, @post.topic_id, Post.types[:regular])
|
Post
|
||||||
.order("post_number DESC")
|
.where(
|
||||||
.limit(1)
|
"post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden",
|
||||||
.pluck(:cooked)
|
@post.post_number,
|
||||||
.first
|
@post.topic_id,
|
||||||
|
Post.types[:regular],
|
||||||
|
)
|
||||||
|
.order("post_number DESC")
|
||||||
|
.limit(1)
|
||||||
|
.pluck(:cooked)
|
||||||
|
.first
|
||||||
|
|
||||||
return if previous.blank?
|
return if previous.blank?
|
||||||
|
|
||||||
previous_text = Nokogiri::HTML5::fragment(previous).text.strip
|
previous_text = Nokogiri::HTML5.fragment(previous).text.strip
|
||||||
quoted_text = @doc.css("aside.quote:first-child blockquote").first&.text&.strip || ""
|
quoted_text = @doc.css("aside.quote:first-child blockquote").first&.text&.strip || ""
|
||||||
|
|
||||||
return if previous_text.gsub(/(\s){2,}/, '\1') != quoted_text.gsub(/(\s){2,}/, '\1')
|
return if previous_text.gsub(/(\s){2,}/, '\1') != quoted_text.gsub(/(\s){2,}/, '\1')
|
||||||
|
|
||||||
quote_regexp = /\A\s*\[quote.+\[\/quote\]/im
|
quote_regexp = %r{\A\s*\[quote.+\[/quote\]}im
|
||||||
quoteless_raw = @post.raw.sub(quote_regexp, "").strip
|
quoteless_raw = @post.raw.sub(quote_regexp, "").strip
|
||||||
|
|
||||||
return if @post.raw.strip == quoteless_raw
|
return if @post.raw.strip == quoteless_raw
|
||||||
|
|
||||||
PostRevisor.new(@post).revise!(
|
PostRevisor.new(@post).revise!(
|
||||||
Discourse.system_user,
|
Discourse.system_user,
|
||||||
{
|
{ raw: quoteless_raw, edit_reason: I18n.t(:removed_direct_reply_full_quotes) },
|
||||||
raw: quoteless_raw,
|
|
||||||
edit_reason: I18n.t(:removed_direct_reply_full_quotes)
|
|
||||||
},
|
|
||||||
skip_validations: true,
|
skip_validations: true,
|
||||||
bypass_bump: true
|
bypass_bump: true,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_images
|
def extract_images
|
||||||
# all images with a src attribute
|
# all images with a src attribute
|
||||||
@doc.css("img[src], img[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]") -
|
@doc.css("img[src], img[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]") -
|
||||||
# minus data images
|
# minus data images
|
||||||
@doc.css("img[src^='data']") -
|
@doc.css("img[src^='data']") -
|
||||||
# minus emojis
|
# minus emojis
|
||||||
@doc.css("img.emoji")
|
@doc.css("img.emoji")
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_images_for_post
|
def extract_images_for_post
|
||||||
# all images with a src attribute
|
# all images with a src attribute
|
||||||
@doc.css("img[src]") -
|
@doc.css("img[src]") -
|
||||||
# minus emojis
|
# minus emojis
|
||||||
@doc.css("img.emoji") -
|
@doc.css("img.emoji") -
|
||||||
# minus images inside quotes
|
# minus images inside quotes
|
||||||
@doc.css(".quote img") -
|
@doc.css(".quote img") -
|
||||||
# minus onebox site icons
|
# minus onebox site icons
|
||||||
@doc.css("img.site-icon") -
|
@doc.css("img.site-icon") -
|
||||||
# minus onebox avatars
|
# minus onebox avatars
|
||||||
@doc.css("img.onebox-avatar") -
|
@doc.css("img.onebox-avatar") - @doc.css("img.onebox-avatar-inline") -
|
||||||
@doc.css("img.onebox-avatar-inline") -
|
# minus github onebox profile images
|
||||||
# minus github onebox profile images
|
@doc.css(".onebox.githubfolder img")
|
||||||
@doc.css(".onebox.githubfolder img")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def convert_to_link!(img)
|
def convert_to_link!(img)
|
||||||
w, h = img["width"].to_i, img["height"].to_i
|
w, h = img["width"].to_i, img["height"].to_i
|
||||||
user_width, user_height = (w > 0 && h > 0 && [w, h]) ||
|
user_width, user_height =
|
||||||
get_size_from_attributes(img) ||
|
(w > 0 && h > 0 && [w, h]) || get_size_from_attributes(img) ||
|
||||||
get_size_from_image_sizes(img["src"], @opts[:image_sizes])
|
get_size_from_image_sizes(img["src"], @opts[:image_sizes])
|
||||||
|
|
||||||
limit_size!(img)
|
limit_size!(img)
|
||||||
|
|
||||||
@ -155,7 +159,7 @@ class CookedPostProcessor
|
|||||||
|
|
||||||
upload = Upload.get_from_url(src)
|
upload = Upload.get_from_url(src)
|
||||||
|
|
||||||
original_width, original_height = nil
|
original_width, original_height = nil
|
||||||
|
|
||||||
if (upload.present?)
|
if (upload.present?)
|
||||||
original_width = upload.width || 0
|
original_width = upload.width || 0
|
||||||
@ -172,12 +176,17 @@ class CookedPostProcessor
|
|||||||
img.add_class("animated")
|
img.add_class("animated")
|
||||||
end
|
end
|
||||||
|
|
||||||
return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height
|
if original_width <= SiteSetting.max_image_width &&
|
||||||
|
original_height <= SiteSetting.max_image_height
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0
|
user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 &&
|
||||||
|
user_height.to_i <= 0
|
||||||
width, height = user_width, user_height
|
width, height = user_width, user_height
|
||||||
|
|
||||||
crop = SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop
|
crop =
|
||||||
|
SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop
|
||||||
|
|
||||||
if crop
|
if crop
|
||||||
width, height = ImageSizer.crop(width, height)
|
width, height = ImageSizer.crop(width, height)
|
||||||
@ -200,7 +209,7 @@ class CookedPostProcessor
|
|||||||
|
|
||||||
return if upload.animated?
|
return if upload.animated?
|
||||||
|
|
||||||
if img.ancestors('.onebox, .onebox-body, .quote').blank? && !img.classes.include?("onebox")
|
if img.ancestors(".onebox, .onebox-body, .quote").blank? && !img.classes.include?("onebox")
|
||||||
add_lightbox!(img, original_width, original_height, upload, cropped: crop)
|
add_lightbox!(img, original_width, original_height, upload, cropped: crop)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -211,7 +220,7 @@ class CookedPostProcessor
|
|||||||
def each_responsive_ratio
|
def each_responsive_ratio
|
||||||
SiteSetting
|
SiteSetting
|
||||||
.responsive_post_image_sizes
|
.responsive_post_image_sizes
|
||||||
.split('|')
|
.split("|")
|
||||||
.map(&:to_f)
|
.map(&:to_f)
|
||||||
.sort
|
.sort
|
||||||
.each { |r| yield r if r > 1 }
|
.each { |r| yield r if r > 1 }
|
||||||
@ -239,13 +248,16 @@ class CookedPostProcessor
|
|||||||
srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x"
|
srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x"
|
||||||
end
|
end
|
||||||
|
|
||||||
img["srcset"] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present?
|
img[
|
||||||
|
"srcset"
|
||||||
|
] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present?
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
img["src"] = upload.url
|
img["src"] = upload.url
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@disable_dominant_color && (color = upload.dominant_color(calculate_if_missing: true).presence)
|
if !@disable_dominant_color &&
|
||||||
|
(color = upload.dominant_color(calculate_if_missing: true).presence)
|
||||||
img["data-dominant-color"] = color
|
img["data-dominant-color"] = color
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -261,9 +273,7 @@ class CookedPostProcessor
|
|||||||
a = create_link_node("lightbox", src)
|
a = create_link_node("lightbox", src)
|
||||||
img.add_next_sibling(a)
|
img.add_next_sibling(a)
|
||||||
|
|
||||||
if upload
|
a["data-download-href"] = Discourse.store.download_url(upload) if upload
|
||||||
a["data-download-href"] = Discourse.store.download_url(upload)
|
|
||||||
end
|
|
||||||
|
|
||||||
a.add_child(img)
|
a.add_child(img)
|
||||||
|
|
||||||
@ -309,48 +319,55 @@ class CookedPostProcessor
|
|||||||
@post.update_column(:image_upload_id, upload.id) # post
|
@post.update_column(:image_upload_id, upload.id) # post
|
||||||
if @post.is_first_post? # topic
|
if @post.is_first_post? # topic
|
||||||
@post.topic.update_column(:image_upload_id, upload.id)
|
@post.topic.update_column(:image_upload_id, upload.id)
|
||||||
extra_sizes = ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes
|
extra_sizes =
|
||||||
|
ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes
|
||||||
@post.topic.generate_thumbnails!(extra_sizes: extra_sizes)
|
@post.topic.generate_thumbnails!(extra_sizes: extra_sizes)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@post.update_column(:image_upload_id, nil) if @post.image_upload_id
|
@post.update_column(:image_upload_id, nil) if @post.image_upload_id
|
||||||
@post.topic.update_column(:image_upload_id, nil) if @post.topic.image_upload_id && @post.is_first_post?
|
if @post.topic.image_upload_id && @post.is_first_post?
|
||||||
|
@post.topic.update_column(:image_upload_id, nil)
|
||||||
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def optimize_urls
|
def optimize_urls
|
||||||
%w{href data-download-href}.each do |selector|
|
%w[href data-download-href].each do |selector|
|
||||||
@doc.css("a[#{selector}]").each do |a|
|
@doc.css("a[#{selector}]").each { |a| a[selector] = UrlHelper.cook_url(a[selector].to_s) }
|
||||||
a[selector] = UrlHelper.cook_url(a[selector].to_s)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
%w{src}.each do |selector|
|
%w[src].each do |selector|
|
||||||
@doc.css("img[#{selector}]").each do |img|
|
@doc
|
||||||
custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"])
|
.css("img[#{selector}]")
|
||||||
img[selector] = UrlHelper.cook_url(
|
.each do |img|
|
||||||
img[selector].to_s, secure: @post.with_secure_uploads? && !custom_emoji
|
custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"])
|
||||||
)
|
img[selector] = UrlHelper.cook_url(
|
||||||
end
|
img[selector].to_s,
|
||||||
|
secure: @post.with_secure_uploads? && !custom_emoji,
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_user_ids
|
def remove_user_ids
|
||||||
@doc.css("a[href]").each do |a|
|
@doc
|
||||||
uri = begin
|
.css("a[href]")
|
||||||
URI(a["href"])
|
.each do |a|
|
||||||
rescue URI::Error
|
uri =
|
||||||
next
|
begin
|
||||||
|
URI(a["href"])
|
||||||
|
rescue URI::Error
|
||||||
|
next
|
||||||
|
end
|
||||||
|
next if uri.hostname != Discourse.current_hostname
|
||||||
|
|
||||||
|
query = Rack::Utils.parse_nested_query(uri.query)
|
||||||
|
next if !query.delete("u")
|
||||||
|
|
||||||
|
uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence
|
||||||
|
a["href"] = uri.to_s
|
||||||
end
|
end
|
||||||
next if uri.hostname != Discourse.current_hostname
|
|
||||||
|
|
||||||
query = Rack::Utils.parse_nested_query(uri.query)
|
|
||||||
next if !query.delete("u")
|
|
||||||
|
|
||||||
uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence
|
|
||||||
a["href"] = uri.to_s
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def enforce_nofollow
|
def enforce_nofollow
|
||||||
@ -369,13 +386,14 @@ class CookedPostProcessor
|
|||||||
|
|
||||||
def process_hotlinked_image(img)
|
def process_hotlinked_image(img)
|
||||||
@hotlinked_map ||= @post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h
|
@hotlinked_map ||= @post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h
|
||||||
normalized_src = PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR])
|
normalized_src =
|
||||||
|
PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR])
|
||||||
info = @hotlinked_map[normalized_src]
|
info = @hotlinked_map[normalized_src]
|
||||||
|
|
||||||
still_an_image = true
|
still_an_image = true
|
||||||
|
|
||||||
if info&.too_large?
|
if info&.too_large?
|
||||||
if img.ancestors('.onebox, .onebox-body').blank?
|
if img.ancestors(".onebox, .onebox-body").blank?
|
||||||
add_large_image_placeholder!(img)
|
add_large_image_placeholder!(img)
|
||||||
else
|
else
|
||||||
img.remove
|
img.remove
|
||||||
@ -383,7 +401,7 @@ class CookedPostProcessor
|
|||||||
|
|
||||||
still_an_image = false
|
still_an_image = false
|
||||||
elsif info&.download_failed?
|
elsif info&.download_failed?
|
||||||
if img.ancestors('.onebox, .onebox-body').blank?
|
if img.ancestors(".onebox, .onebox-body").blank?
|
||||||
add_broken_image_placeholder!(img)
|
add_broken_image_placeholder!(img)
|
||||||
else
|
else
|
||||||
img.remove
|
img.remove
|
||||||
@ -399,28 +417,29 @@ class CookedPostProcessor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def add_blocked_hotlinked_media_placeholders
|
def add_blocked_hotlinked_media_placeholders
|
||||||
@doc.css([
|
@doc
|
||||||
"[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]",
|
.css(
|
||||||
"[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]",
|
[
|
||||||
].join(',')).each do |el|
|
"[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]",
|
||||||
src = el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] ||
|
"[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]",
|
||||||
el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(',')&.first&.split(' ')&.first
|
].join(","),
|
||||||
|
)
|
||||||
|
.each do |el|
|
||||||
|
src =
|
||||||
|
el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] ||
|
||||||
|
el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(",")&.first&.split(" ")&.first
|
||||||
|
|
||||||
if el.name == "img"
|
if el.name == "img"
|
||||||
add_blocked_hotlinked_image_placeholder!(el)
|
add_blocked_hotlinked_image_placeholder!(el)
|
||||||
next
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
el = el.parent if %w[video audio].include?(el.parent.name)
|
||||||
|
|
||||||
|
el = el.parent if el.parent.classes.include?("video-container")
|
||||||
|
|
||||||
|
add_blocked_hotlinked_media_placeholder!(el, src)
|
||||||
end
|
end
|
||||||
|
|
||||||
if ["video", "audio"].include?(el.parent.name)
|
|
||||||
el = el.parent
|
|
||||||
end
|
|
||||||
|
|
||||||
if el.parent.classes.include?("video-container")
|
|
||||||
el = el.parent
|
|
||||||
end
|
|
||||||
|
|
||||||
add_blocked_hotlinked_media_placeholder!(el, src)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_svg?(img)
|
def is_svg?(img)
|
||||||
@ -431,6 +450,6 @@ class CookedPostProcessor
|
|||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
File.extname(path) == '.svg' if path
|
File.extname(path) == ".svg" if path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module CookedProcessorMixin
|
module CookedProcessorMixin
|
||||||
|
|
||||||
def post_process_oneboxes
|
def post_process_oneboxes
|
||||||
limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size
|
limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size
|
||||||
oneboxes = {}
|
oneboxes = {}
|
||||||
@ -14,7 +13,7 @@ module CookedProcessorMixin
|
|||||||
|
|
||||||
if skip_onebox
|
if skip_onebox
|
||||||
if is_onebox
|
if is_onebox
|
||||||
element.remove_class('onebox')
|
element.remove_class("onebox")
|
||||||
else
|
else
|
||||||
remove_inline_onebox_loading_class(element)
|
remove_inline_onebox_loading_class(element)
|
||||||
end
|
end
|
||||||
@ -26,11 +25,13 @@ module CookedProcessorMixin
|
|||||||
map[url] = true
|
map[url] = true
|
||||||
|
|
||||||
if is_onebox
|
if is_onebox
|
||||||
onebox = Oneboxer.onebox(url,
|
onebox =
|
||||||
invalidate_oneboxes: !!@opts[:invalidate_oneboxes],
|
Oneboxer.onebox(
|
||||||
user_id: @model&.user_id,
|
url,
|
||||||
category_id: @category_id
|
invalidate_oneboxes: !!@opts[:invalidate_oneboxes],
|
||||||
)
|
user_id: @model&.user_id,
|
||||||
|
category_id: @category_id,
|
||||||
|
)
|
||||||
|
|
||||||
@has_oneboxes = true if onebox.present?
|
@has_oneboxes = true if onebox.present?
|
||||||
onebox
|
onebox
|
||||||
@ -56,7 +57,7 @@ module CookedProcessorMixin
|
|||||||
# and wrap in a div
|
# and wrap in a div
|
||||||
limit_size!(img)
|
limit_size!(img)
|
||||||
|
|
||||||
next if img["class"]&.include?('onebox-avatar')
|
next if img["class"]&.include?("onebox-avatar")
|
||||||
|
|
||||||
parent = parent&.parent if parent&.name == "a"
|
parent = parent&.parent if parent&.name == "a"
|
||||||
parent_class = parent && parent["class"]
|
parent_class = parent && parent["class"]
|
||||||
@ -84,12 +85,18 @@ module CookedProcessorMixin
|
|||||||
if width < 64 && height < 64
|
if width < 64 && height < 64
|
||||||
img["class"] = img["class"].to_s + " onebox-full-image"
|
img["class"] = img["class"].to_s + " onebox-full-image"
|
||||||
else
|
else
|
||||||
img.delete('width')
|
img.delete("width")
|
||||||
img.delete('height')
|
img.delete("height")
|
||||||
new_parent = img.add_next_sibling("<div class='aspect-image' style='--aspect-ratio:#{width}/#{height};'/>")
|
new_parent =
|
||||||
|
img.add_next_sibling(
|
||||||
|
"<div class='aspect-image' style='--aspect-ratio:#{width}/#{height};'/>",
|
||||||
|
)
|
||||||
new_parent.first.add_child(img)
|
new_parent.first.add_child(img)
|
||||||
end
|
end
|
||||||
elsif (parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || parent_class&.include?("scale-images")) && width > 0 && height > 0
|
elsif (
|
||||||
|
parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") ||
|
||||||
|
parent_class&.include?("scale-images")
|
||||||
|
) && width > 0 && height > 0
|
||||||
img.remove_attribute("width")
|
img.remove_attribute("width")
|
||||||
img.remove_attribute("height")
|
img.remove_attribute("height")
|
||||||
parent["class"] = "aspect-image-full-size"
|
parent["class"] = "aspect-image-full-size"
|
||||||
@ -98,16 +105,18 @@ module CookedProcessorMixin
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content
|
if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content
|
||||||
@doc.css(".onebox-body a[rel], .onebox a[rel]").each do |a|
|
@doc
|
||||||
rel_values = a['rel'].split(' ').map(&:downcase)
|
.css(".onebox-body a[rel], .onebox a[rel]")
|
||||||
rel_values.delete('nofollow')
|
.each do |a|
|
||||||
rel_values.delete('ugc')
|
rel_values = a["rel"].split(" ").map(&:downcase)
|
||||||
if rel_values.blank?
|
rel_values.delete("nofollow")
|
||||||
a.remove_attribute("rel")
|
rel_values.delete("ugc")
|
||||||
else
|
if rel_values.blank?
|
||||||
a["rel"] = rel_values.join(' ')
|
a.remove_attribute("rel")
|
||||||
|
else
|
||||||
|
a["rel"] = rel_values.join(" ")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -116,9 +125,9 @@ module CookedProcessorMixin
|
|||||||
# 1) the width/height attributes
|
# 1) the width/height attributes
|
||||||
# 2) the dimension from the preview (image_sizes)
|
# 2) the dimension from the preview (image_sizes)
|
||||||
# 3) the dimension of the original image (HTTP request)
|
# 3) the dimension of the original image (HTTP request)
|
||||||
w, h = get_size_from_attributes(img) ||
|
w, h =
|
||||||
get_size_from_image_sizes(img["src"], @opts[:image_sizes]) ||
|
get_size_from_attributes(img) || get_size_from_image_sizes(img["src"], @opts[:image_sizes]) ||
|
||||||
get_size(img["src"])
|
get_size(img["src"])
|
||||||
|
|
||||||
# limit the size of the thumbnail
|
# limit the size of the thumbnail
|
||||||
img["width"], img["height"] = ImageSizer.resize(w, h)
|
img["width"], img["height"] = ImageSizer.resize(w, h)
|
||||||
@ -126,7 +135,7 @@ module CookedProcessorMixin
|
|||||||
|
|
||||||
def get_size_from_attributes(img)
|
def get_size_from_attributes(img)
|
||||||
w, h = img["width"].to_i, img["height"].to_i
|
w, h = img["width"].to_i, img["height"].to_i
|
||||||
return [w, h] unless w <= 0 || h <= 0
|
return w, h unless w <= 0 || h <= 0
|
||||||
# if only width or height are specified attempt to scale image
|
# if only width or height are specified attempt to scale image
|
||||||
if w > 0 || h > 0
|
if w > 0 || h > 0
|
||||||
w = w.to_f
|
w = w.to_f
|
||||||
@ -149,9 +158,9 @@ module CookedProcessorMixin
|
|||||||
return unless image_sizes.present?
|
return unless image_sizes.present?
|
||||||
image_sizes.each do |image_size|
|
image_sizes.each do |image_size|
|
||||||
url, size = image_size[0], image_size[1]
|
url, size = image_size[0], image_size[1]
|
||||||
if url && src && url.include?(src) &&
|
if url && src && url.include?(src) && size && size["width"].to_i > 0 &&
|
||||||
size && size["width"].to_i > 0 && size["height"].to_i > 0
|
size["height"].to_i > 0
|
||||||
return [size["width"], size["height"]]
|
return size["width"], size["height"]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
@ -165,7 +174,7 @@ module CookedProcessorMixin
|
|||||||
return @size_cache[url] if @size_cache.has_key?(url)
|
return @size_cache[url] if @size_cache.has_key?(url)
|
||||||
|
|
||||||
absolute_url = url
|
absolute_url = url
|
||||||
absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ /^\/[^\/]/
|
absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ %r{^/[^/]}
|
||||||
|
|
||||||
return unless absolute_url
|
return unless absolute_url
|
||||||
|
|
||||||
@ -186,14 +195,13 @@ module CookedProcessorMixin
|
|||||||
else
|
else
|
||||||
@size_cache[url] = FastImage.size(absolute_url)
|
@size_cache[url] = FastImage.size(absolute_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError
|
rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError
|
||||||
# FastImage.size raises BufError for some gifs, leave it.
|
# FastImage.size raises BufError for some gifs, leave it.
|
||||||
end
|
end
|
||||||
|
|
||||||
def is_valid_image_url?(url)
|
def is_valid_image_url?(url)
|
||||||
uri = URI.parse(url)
|
uri = URI.parse(url)
|
||||||
%w(http https).include? uri.scheme
|
%w[http https].include? uri.scheme
|
||||||
rescue URI::Error
|
rescue URI::Error
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -217,9 +225,12 @@ module CookedProcessorMixin
|
|||||||
"help",
|
"help",
|
||||||
I18n.t(
|
I18n.t(
|
||||||
"upload.placeholders.too_large_humanized",
|
"upload.placeholders.too_large_humanized",
|
||||||
max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_image_size_kb.kilobytes)
|
max_size:
|
||||||
)
|
ActiveSupport::NumberHelper.number_to_human_size(
|
||||||
)
|
SiteSetting.max_image_size_kb.kilobytes,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Only if the image is already linked
|
# Only if the image is already linked
|
||||||
@ -227,7 +238,7 @@ module CookedProcessorMixin
|
|||||||
parent = placeholder.parent
|
parent = placeholder.parent
|
||||||
parent.add_next_sibling(placeholder)
|
parent.add_next_sibling(placeholder)
|
||||||
|
|
||||||
if parent.name == 'a' && parent["href"].present?
|
if parent.name == "a" && parent["href"].present?
|
||||||
if url == parent["href"]
|
if url == parent["href"]
|
||||||
parent.remove
|
parent.remove
|
||||||
else
|
else
|
||||||
@ -295,12 +306,13 @@ module CookedProcessorMixin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def process_inline_onebox(element)
|
def process_inline_onebox(element)
|
||||||
inline_onebox = InlineOneboxer.lookup(
|
inline_onebox =
|
||||||
element.attributes["href"].value,
|
InlineOneboxer.lookup(
|
||||||
invalidate: !!@opts[:invalidate_oneboxes],
|
element.attributes["href"].value,
|
||||||
user_id: @model&.user_id,
|
invalidate: !!@opts[:invalidate_oneboxes],
|
||||||
category_id: @category_id
|
user_id: @model&.user_id,
|
||||||
)
|
category_id: @category_id,
|
||||||
|
)
|
||||||
|
|
||||||
if title = inline_onebox&.dig(:title)
|
if title = inline_onebox&.dig(:title)
|
||||||
element.children = CGI.escapeHTML(title)
|
element.children = CGI.escapeHTML(title)
|
||||||
|
@ -4,7 +4,7 @@ module CrawlerDetection
|
|||||||
WAYBACK_MACHINE_URL = "archive.org"
|
WAYBACK_MACHINE_URL = "archive.org"
|
||||||
|
|
||||||
def self.to_matcher(string, type: nil)
|
def self.to_matcher(string, type: nil)
|
||||||
escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|')
|
escaped = string.split("|").map { |agent| Regexp.escape(agent) }.join("|")
|
||||||
|
|
||||||
if type == :real && Rails.env == "test"
|
if type == :real && Rails.env == "test"
|
||||||
# we need this bypass so we properly render views
|
# we need this bypass so we properly render views
|
||||||
@ -15,18 +15,33 @@ module CrawlerDetection
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.crawler?(user_agent, via_header = nil)
|
def self.crawler?(user_agent, via_header = nil)
|
||||||
return true if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || via_header&.include?(WAYBACK_MACHINE_URL)
|
if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) ||
|
||||||
|
via_header&.include?(WAYBACK_MACHINE_URL)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
# this is done to avoid regenerating regexes
|
# this is done to avoid regenerating regexes
|
||||||
@non_crawler_matchers ||= {}
|
@non_crawler_matchers ||= {}
|
||||||
@matchers ||= {}
|
@matchers ||= {}
|
||||||
|
|
||||||
possibly_real = (@non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher(SiteSetting.non_crawler_user_agents, type: :real))
|
possibly_real =
|
||||||
|
(
|
||||||
|
@non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher(
|
||||||
|
SiteSetting.non_crawler_user_agents,
|
||||||
|
type: :real,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if user_agent.match?(possibly_real)
|
if user_agent.match?(possibly_real)
|
||||||
known_bots = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents))
|
known_bots =
|
||||||
|
(@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents))
|
||||||
if user_agent.match?(known_bots)
|
if user_agent.match?(known_bots)
|
||||||
bypass = (@matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher(SiteSetting.crawler_check_bypass_agents))
|
bypass =
|
||||||
|
(
|
||||||
|
@matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher(
|
||||||
|
SiteSetting.crawler_check_bypass_agents,
|
||||||
|
)
|
||||||
|
)
|
||||||
!user_agent.match?(bypass)
|
!user_agent.match?(bypass)
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
@ -34,30 +49,40 @@ module CrawlerDetection
|
|||||||
else
|
else
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.show_browser_update?(user_agent)
|
def self.show_browser_update?(user_agent)
|
||||||
return false if SiteSetting.browser_update_user_agents.blank?
|
return false if SiteSetting.browser_update_user_agents.blank?
|
||||||
|
|
||||||
@browser_update_matchers ||= {}
|
@browser_update_matchers ||= {}
|
||||||
matcher = @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher(SiteSetting.browser_update_user_agents)
|
matcher =
|
||||||
|
@browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher(
|
||||||
|
SiteSetting.browser_update_user_agents,
|
||||||
|
)
|
||||||
user_agent.match?(matcher)
|
user_agent.match?(matcher)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Given a user_agent that returns true from crawler?, should its request be allowed?
|
# Given a user_agent that returns true from crawler?, should its request be allowed?
|
||||||
def self.allow_crawler?(user_agent)
|
def self.allow_crawler?(user_agent)
|
||||||
return true if SiteSetting.allowed_crawler_user_agents.blank? &&
|
if SiteSetting.allowed_crawler_user_agents.blank? &&
|
||||||
SiteSetting.blocked_crawler_user_agents.blank?
|
SiteSetting.blocked_crawler_user_agents.blank?
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
@allowlisted_matchers ||= {}
|
@allowlisted_matchers ||= {}
|
||||||
@blocklisted_matchers ||= {}
|
@blocklisted_matchers ||= {}
|
||||||
|
|
||||||
if SiteSetting.allowed_crawler_user_agents.present?
|
if SiteSetting.allowed_crawler_user_agents.present?
|
||||||
allowlisted = @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher(SiteSetting.allowed_crawler_user_agents)
|
allowlisted =
|
||||||
|
@allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher(
|
||||||
|
SiteSetting.allowed_crawler_user_agents,
|
||||||
|
)
|
||||||
!user_agent.nil? && user_agent.match?(allowlisted)
|
!user_agent.nil? && user_agent.match?(allowlisted)
|
||||||
else
|
else
|
||||||
blocklisted = @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher(SiteSetting.blocked_crawler_user_agents)
|
blocklisted =
|
||||||
|
@blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher(
|
||||||
|
SiteSetting.blocked_crawler_user_agents,
|
||||||
|
)
|
||||||
user_agent.nil? || !user_agent.match?(blocklisted)
|
user_agent.nil? || !user_agent.match?(blocklisted)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
# Provides a way to check a CSRF token outside of a controller
|
# Provides a way to check a CSRF token outside of a controller
|
||||||
class CSRFTokenVerifier
|
class CSRFTokenVerifier
|
||||||
class InvalidCSRFToken < StandardError; end
|
class InvalidCSRFToken < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
include ActiveSupport::Configurable
|
include ActiveSupport::Configurable
|
||||||
include ActionController::RequestForgeryProtection
|
include ActionController::RequestForgeryProtection
|
||||||
@ -18,9 +19,7 @@ class CSRFTokenVerifier
|
|||||||
def call(env)
|
def call(env)
|
||||||
@request = ActionDispatch::Request.new(env.dup)
|
@request = ActionDispatch::Request.new(env.dup)
|
||||||
|
|
||||||
unless verified_request?
|
raise InvalidCSRFToken unless verified_request?
|
||||||
raise InvalidCSRFToken
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
public :form_authenticity_token
|
public :form_authenticity_token
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module CurrentUser
|
module CurrentUser
|
||||||
|
|
||||||
def self.has_auth_cookie?(env)
|
def self.has_auth_cookie?(env)
|
||||||
Discourse.current_user_provider.new(env).has_auth_cookie?
|
Discourse.current_user_provider.new(env).has_auth_cookie?
|
||||||
end
|
end
|
||||||
@ -45,5 +44,4 @@ module CurrentUser
|
|||||||
def current_user_provider
|
def current_user_provider
|
||||||
@current_user_provider ||= Discourse.current_user_provider.new(request.env)
|
@current_user_provider ||= Discourse.current_user_provider.new(request.env)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
108
lib/db_helper.rb
108
lib/db_helper.rb
@ -3,7 +3,6 @@
|
|||||||
require "migration/base_dropper"
|
require "migration/base_dropper"
|
||||||
|
|
||||||
class DbHelper
|
class DbHelper
|
||||||
|
|
||||||
REMAP_SQL ||= <<~SQL
|
REMAP_SQL ||= <<~SQL
|
||||||
SELECT table_name::text, column_name::text, character_maximum_length
|
SELECT table_name::text, column_name::text, character_maximum_length
|
||||||
FROM information_schema.columns
|
FROM information_schema.columns
|
||||||
@ -19,24 +18,33 @@ class DbHelper
|
|||||||
WHERE trigger_name LIKE '%_readonly'
|
WHERE trigger_name LIKE '%_readonly'
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
TRUNCATABLE_COLUMNS ||= [
|
TRUNCATABLE_COLUMNS ||= ["topic_links.url"]
|
||||||
'topic_links.url'
|
|
||||||
]
|
|
||||||
|
|
||||||
def self.remap(from, to, anchor_left: false, anchor_right: false, excluded_tables: [], verbose: false)
|
def self.remap(
|
||||||
like = "#{anchor_left ? '' : "%"}#{from}#{anchor_right ? '' : "%"}"
|
from,
|
||||||
|
to,
|
||||||
|
anchor_left: false,
|
||||||
|
anchor_right: false,
|
||||||
|
excluded_tables: [],
|
||||||
|
verbose: false
|
||||||
|
)
|
||||||
|
like = "#{anchor_left ? "" : "%"}#{from}#{anchor_right ? "" : "%"}"
|
||||||
text_columns = find_text_columns(excluded_tables)
|
text_columns = find_text_columns(excluded_tables)
|
||||||
|
|
||||||
text_columns.each do |table, columns|
|
text_columns.each do |table, columns|
|
||||||
set = columns.map do |column|
|
set =
|
||||||
replace = "REPLACE(\"#{column[:name]}\", :from, :to)"
|
columns
|
||||||
replace = truncate(replace, table, column)
|
.map do |column|
|
||||||
"\"#{column[:name]}\" = #{replace}"
|
replace = "REPLACE(\"#{column[:name]}\", :from, :to)"
|
||||||
end.join(", ")
|
replace = truncate(replace, table, column)
|
||||||
|
"\"#{column[:name]}\" = #{replace}"
|
||||||
|
end
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
where = columns.map do |column|
|
where =
|
||||||
"\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like"
|
columns
|
||||||
end.join(" OR ")
|
.map { |column| "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" }
|
||||||
|
.join(" OR ")
|
||||||
|
|
||||||
rows = DB.exec(<<~SQL, from: from, to: to, like: like)
|
rows = DB.exec(<<~SQL, from: from, to: to, like: like)
|
||||||
UPDATE \"#{table}\"
|
UPDATE \"#{table}\"
|
||||||
@ -50,19 +58,32 @@ class DbHelper
|
|||||||
finish!
|
finish!
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.regexp_replace(pattern, replacement, flags: "gi", match: "~*", excluded_tables: [], verbose: false)
|
def self.regexp_replace(
|
||||||
|
pattern,
|
||||||
|
replacement,
|
||||||
|
flags: "gi",
|
||||||
|
match: "~*",
|
||||||
|
excluded_tables: [],
|
||||||
|
verbose: false
|
||||||
|
)
|
||||||
text_columns = find_text_columns(excluded_tables)
|
text_columns = find_text_columns(excluded_tables)
|
||||||
|
|
||||||
text_columns.each do |table, columns|
|
text_columns.each do |table, columns|
|
||||||
set = columns.map do |column|
|
set =
|
||||||
replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)"
|
columns
|
||||||
replace = truncate(replace, table, column)
|
.map do |column|
|
||||||
"\"#{column[:name]}\" = #{replace}"
|
replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)"
|
||||||
end.join(", ")
|
replace = truncate(replace, table, column)
|
||||||
|
"\"#{column[:name]}\" = #{replace}"
|
||||||
|
end
|
||||||
|
.join(", ")
|
||||||
|
|
||||||
where = columns.map do |column|
|
where =
|
||||||
"\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern"
|
columns
|
||||||
end.join(" OR ")
|
.map do |column|
|
||||||
|
"\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern"
|
||||||
|
end
|
||||||
|
.join(" OR ")
|
||||||
|
|
||||||
rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match)
|
rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match)
|
||||||
UPDATE \"#{table}\"
|
UPDATE \"#{table}\"
|
||||||
@ -78,23 +99,25 @@ class DbHelper
|
|||||||
|
|
||||||
def self.find(needle, anchor_left: false, anchor_right: false, excluded_tables: [])
|
def self.find(needle, anchor_left: false, anchor_right: false, excluded_tables: [])
|
||||||
found = {}
|
found = {}
|
||||||
like = "#{anchor_left ? '' : "%"}#{needle}#{anchor_right ? '' : "%"}"
|
like = "#{anchor_left ? "" : "%"}#{needle}#{anchor_right ? "" : "%"}"
|
||||||
|
|
||||||
DB.query(REMAP_SQL).each do |r|
|
DB
|
||||||
next if excluded_tables.include?(r.table_name)
|
.query(REMAP_SQL)
|
||||||
|
.each do |r|
|
||||||
|
next if excluded_tables.include?(r.table_name)
|
||||||
|
|
||||||
rows = DB.query(<<~SQL, like: like)
|
rows = DB.query(<<~SQL, like: like)
|
||||||
SELECT \"#{r.column_name}\"
|
SELECT \"#{r.column_name}\"
|
||||||
FROM \"#{r.table_name}\"
|
FROM \"#{r.table_name}\"
|
||||||
WHERE \""#{r.column_name}"\" LIKE :like
|
WHERE \""#{r.column_name}"\" LIKE :like
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
if rows.size > 0
|
if rows.size > 0
|
||||||
found["#{r.table_name}.#{r.column_name}"] = rows.map do |row|
|
found["#{r.table_name}.#{r.column_name}"] = rows.map do |row|
|
||||||
row.public_send(r.column_name)
|
row.public_send(r.column_name)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
found
|
found
|
||||||
end
|
end
|
||||||
@ -112,16 +135,21 @@ class DbHelper
|
|||||||
triggers = DB.query(TRIGGERS_SQL).map(&:trigger_name).to_set
|
triggers = DB.query(TRIGGERS_SQL).map(&:trigger_name).to_set
|
||||||
text_columns = Hash.new { |h, k| h[k] = [] }
|
text_columns = Hash.new { |h, k| h[k] = [] }
|
||||||
|
|
||||||
DB.query(REMAP_SQL).each do |r|
|
DB
|
||||||
next if excluded_tables.include?(r.table_name) ||
|
.query(REMAP_SQL)
|
||||||
triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name)) ||
|
.each do |r|
|
||||||
triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name))
|
if excluded_tables.include?(r.table_name) ||
|
||||||
|
triggers.include?(
|
||||||
|
Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name),
|
||||||
|
) || triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name))
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
text_columns[r.table_name] << {
|
text_columns[r.table_name] << {
|
||||||
name: r.column_name,
|
name: r.column_name,
|
||||||
max_length: r.character_maximum_length
|
max_length: r.character_maximum_length,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
text_columns
|
text_columns
|
||||||
end
|
end
|
||||||
|
@ -1,26 +1,22 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module Demon; end
|
module Demon
|
||||||
|
end
|
||||||
|
|
||||||
# intelligent fork based demonizer
|
# intelligent fork based demonizer
|
||||||
class Demon::Base
|
class Demon::Base
|
||||||
|
|
||||||
def self.demons
|
def self.demons
|
||||||
@demons
|
@demons
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.start(count = 1, verbose: false)
|
def self.start(count = 1, verbose: false)
|
||||||
@demons ||= {}
|
@demons ||= {}
|
||||||
count.times do |i|
|
count.times { |i| (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start }
|
||||||
(@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stop
|
def self.stop
|
||||||
return unless @demons
|
return unless @demons
|
||||||
@demons.values.each do |demon|
|
@demons.values.each { |demon| demon.stop }
|
||||||
demon.stop
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.restart
|
def self.restart
|
||||||
@ -32,16 +28,12 @@ class Demon::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.ensure_running
|
def self.ensure_running
|
||||||
@demons.values.each do |demon|
|
@demons.values.each { |demon| demon.ensure_running }
|
||||||
demon.ensure_running
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.kill(signal)
|
def self.kill(signal)
|
||||||
return unless @demons
|
return unless @demons
|
||||||
@demons.values.each do |demon|
|
@demons.values.each { |demon| demon.kill(signal) }
|
||||||
demon.kill(signal)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :pid, :parent_pid, :started, :index
|
attr_reader :pid, :parent_pid, :started, :index
|
||||||
@ -83,18 +75,27 @@ class Demon::Base
|
|||||||
if @pid
|
if @pid
|
||||||
Process.kill(stop_signal, @pid)
|
Process.kill(stop_signal, @pid)
|
||||||
|
|
||||||
wait_for_stop = lambda {
|
wait_for_stop =
|
||||||
timeout = @stop_timeout
|
lambda do
|
||||||
|
timeout = @stop_timeout
|
||||||
|
|
||||||
while alive? && timeout > 0
|
while alive? && timeout > 0
|
||||||
timeout -= (@stop_timeout / 10.0)
|
timeout -= (@stop_timeout / 10.0)
|
||||||
sleep(@stop_timeout / 10.0)
|
sleep(@stop_timeout / 10.0)
|
||||||
Process.waitpid(@pid, Process::WNOHANG) rescue -1
|
begin
|
||||||
|
Process.waitpid(@pid, Process::WNOHANG)
|
||||||
|
rescue StandardError
|
||||||
|
-1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
Process.waitpid(@pid, Process::WNOHANG)
|
||||||
|
rescue StandardError
|
||||||
|
-1
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Process.waitpid(@pid, Process::WNOHANG) rescue -1
|
|
||||||
}
|
|
||||||
|
|
||||||
wait_for_stop.call
|
wait_for_stop.call
|
||||||
|
|
||||||
if alive?
|
if alive?
|
||||||
@ -118,7 +119,12 @@ class Demon::Base
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
dead = Process.waitpid(@pid, Process::WNOHANG) rescue -1
|
dead =
|
||||||
|
begin
|
||||||
|
Process.waitpid(@pid, Process::WNOHANG)
|
||||||
|
rescue StandardError
|
||||||
|
-1
|
||||||
|
end
|
||||||
if dead
|
if dead
|
||||||
STDERR.puts "Detected dead worker #{@pid}, restarting..."
|
STDERR.puts "Detected dead worker #{@pid}, restarting..."
|
||||||
@pid = nil
|
@pid = nil
|
||||||
@ -141,21 +147,20 @@ class Demon::Base
|
|||||||
end
|
end
|
||||||
|
|
||||||
def run
|
def run
|
||||||
@pid = fork do
|
@pid =
|
||||||
Process.setproctitle("discourse #{self.class.prefix}")
|
fork do
|
||||||
monitor_parent
|
Process.setproctitle("discourse #{self.class.prefix}")
|
||||||
establish_app
|
monitor_parent
|
||||||
after_fork
|
establish_app
|
||||||
end
|
after_fork
|
||||||
|
end
|
||||||
write_pid_file
|
write_pid_file
|
||||||
end
|
end
|
||||||
|
|
||||||
def already_running?
|
def already_running?
|
||||||
if File.exist? pid_file
|
if File.exist? pid_file
|
||||||
pid = File.read(pid_file).to_i
|
pid = File.read(pid_file).to_i
|
||||||
if Demon::Base.alive?(pid)
|
return pid if Demon::Base.alive?(pid)
|
||||||
return pid
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
nil
|
nil
|
||||||
@ -164,24 +169,20 @@ class Demon::Base
|
|||||||
def self.alive?(pid)
|
def self.alive?(pid)
|
||||||
Process.kill(0, pid)
|
Process.kill(0, pid)
|
||||||
true
|
true
|
||||||
rescue
|
rescue StandardError
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def verbose(msg)
|
def verbose(msg)
|
||||||
if @verbose
|
puts msg if @verbose
|
||||||
puts msg
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def write_pid_file
|
def write_pid_file
|
||||||
verbose("writing pid file #{pid_file} for #{@pid}")
|
verbose("writing pid file #{pid_file} for #{@pid}")
|
||||||
FileUtils.mkdir_p(@rails_root + "tmp/pids")
|
FileUtils.mkdir_p(@rails_root + "tmp/pids")
|
||||||
File.open(pid_file, 'w') do |f|
|
File.open(pid_file, "w") { |f| f.write(@pid) }
|
||||||
f.write(@pid)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_pid_file
|
def delete_pid_file
|
||||||
|
@ -36,15 +36,20 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
status = nil
|
status = nil
|
||||||
idle = false
|
idle = false
|
||||||
|
|
||||||
while @running && group.reload.imap_mailbox_name.present? do
|
while @running && group.reload.imap_mailbox_name.present?
|
||||||
ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group)
|
ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group)
|
||||||
status = syncer.process(
|
status =
|
||||||
idle: syncer.can_idle? && status && status[:remaining] == 0,
|
syncer.process(
|
||||||
old_emails_limit: status && status[:remaining] > 0 ? 0 : nil,
|
idle: syncer.can_idle? && status && status[:remaining] == 0,
|
||||||
)
|
old_emails_limit: status && status[:remaining] > 0 ? 0 : nil,
|
||||||
|
)
|
||||||
|
|
||||||
if !syncer.can_idle? && status[:remaining] == 0
|
if !syncer.can_idle? && status[:remaining] == 0
|
||||||
ImapSyncLog.debug("Going to sleep for group #{group.name} in db #{db} to wait for new emails", group, db: false)
|
ImapSyncLog.debug(
|
||||||
|
"Going to sleep for group #{group.name} in db #{db} to wait for new emails",
|
||||||
|
group,
|
||||||
|
db: false,
|
||||||
|
)
|
||||||
|
|
||||||
# Thread goes into sleep for a bit so it is better to return any
|
# Thread goes into sleep for a bit so it is better to return any
|
||||||
# connection back to the pool.
|
# connection back to the pool.
|
||||||
@ -66,11 +71,7 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
# synchronization primitives available anyway).
|
# synchronization primitives available anyway).
|
||||||
@running = false
|
@running = false
|
||||||
|
|
||||||
@sync_data.each do |db, sync_data|
|
@sync_data.each { |db, sync_data| sync_data.each { |_, data| kill_and_disconnect!(data) } }
|
||||||
sync_data.each do |_, data|
|
|
||||||
kill_and_disconnect!(data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
end
|
end
|
||||||
@ -89,9 +90,9 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
@sync_data = {}
|
@sync_data = {}
|
||||||
@sync_lock = Mutex.new
|
@sync_lock = Mutex.new
|
||||||
|
|
||||||
trap('INT') { kill_threads }
|
trap("INT") { kill_threads }
|
||||||
trap('TERM') { kill_threads }
|
trap("TERM") { kill_threads }
|
||||||
trap('HUP') { kill_threads }
|
trap("HUP") { kill_threads }
|
||||||
|
|
||||||
while @running
|
while @running
|
||||||
Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL)
|
Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL)
|
||||||
@ -101,9 +102,7 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
@sync_data.filter! do |db, sync_data|
|
@sync_data.filter! do |db, sync_data|
|
||||||
next true if all_dbs.include?(db)
|
next true if all_dbs.include?(db)
|
||||||
|
|
||||||
sync_data.each do |_, data|
|
sync_data.each { |_, data| kill_and_disconnect!(data) }
|
||||||
kill_and_disconnect!(data)
|
|
||||||
end
|
|
||||||
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
@ -121,7 +120,10 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected?
|
next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected?
|
||||||
|
|
||||||
if !groups[group_id]
|
if !groups[group_id]
|
||||||
ImapSyncLog.warn("Killing thread for group because mailbox is no longer synced", group_id)
|
ImapSyncLog.warn(
|
||||||
|
"Killing thread for group because mailbox is no longer synced",
|
||||||
|
group_id,
|
||||||
|
)
|
||||||
else
|
else
|
||||||
ImapSyncLog.warn("Thread for group is dead", group_id)
|
ImapSyncLog.warn("Thread for group is dead", group_id)
|
||||||
end
|
end
|
||||||
@ -133,12 +135,13 @@ class Demon::EmailSync < ::Demon::Base
|
|||||||
# Spawn new threads for groups that are now synchronized.
|
# Spawn new threads for groups that are now synchronized.
|
||||||
groups.each do |group_id, group|
|
groups.each do |group_id, group|
|
||||||
if !@sync_data[db][group_id]
|
if !@sync_data[db][group_id]
|
||||||
ImapSyncLog.debug("Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", group, db: false)
|
ImapSyncLog.debug(
|
||||||
|
"Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}",
|
||||||
|
group,
|
||||||
|
db: false,
|
||||||
|
)
|
||||||
|
|
||||||
@sync_data[db][group_id] = {
|
@sync_data[db][group_id] = { thread: start_thread(db, group), syncer: nil }
|
||||||
thread: start_thread(db, group),
|
|
||||||
syncer: nil
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
require "demon/base"
|
require "demon/base"
|
||||||
|
|
||||||
class Demon::RailsAutospec < Demon::Base
|
class Demon::RailsAutospec < Demon::Base
|
||||||
|
|
||||||
def self.prefix
|
def self.prefix
|
||||||
"rails-autospec"
|
"rails-autospec"
|
||||||
end
|
end
|
||||||
@ -17,15 +16,10 @@ class Demon::RailsAutospec < Demon::Base
|
|||||||
def after_fork
|
def after_fork
|
||||||
require "rack"
|
require "rack"
|
||||||
ENV["RAILS_ENV"] = "test"
|
ENV["RAILS_ENV"] = "test"
|
||||||
Rack::Server.start(
|
Rack::Server.start(config: "config.ru", AccessLog: [], Port: ENV["TEST_SERVER_PORT"] || 60_099)
|
||||||
config: "config.ru",
|
|
||||||
AccessLog: [],
|
|
||||||
Port: ENV["TEST_SERVER_PORT"] || 60099,
|
|
||||||
)
|
|
||||||
rescue => e
|
rescue => e
|
||||||
STDERR.puts e.message
|
STDERR.puts e.message
|
||||||
STDERR.puts e.backtrace.join("\n")
|
STDERR.puts e.backtrace.join("\n")
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
require "demon/base"
|
require "demon/base"
|
||||||
|
|
||||||
class Demon::Sidekiq < ::Demon::Base
|
class Demon::Sidekiq < ::Demon::Base
|
||||||
|
|
||||||
def self.prefix
|
def self.prefix
|
||||||
"sidekiq"
|
"sidekiq"
|
||||||
end
|
end
|
||||||
@ -26,7 +25,7 @@ class Demon::Sidekiq < ::Demon::Base
|
|||||||
Demon::Sidekiq.after_fork&.call
|
Demon::Sidekiq.after_fork&.call
|
||||||
|
|
||||||
puts "Loading Sidekiq in process id #{Process.pid}"
|
puts "Loading Sidekiq in process id #{Process.pid}"
|
||||||
require 'sidekiq/cli'
|
require "sidekiq/cli"
|
||||||
cli = Sidekiq::CLI.instance
|
cli = Sidekiq::CLI.instance
|
||||||
|
|
||||||
# Unicorn uses USR1 to indicate that log files have been rotated
|
# Unicorn uses USR1 to indicate that log files have been rotated
|
||||||
@ -38,10 +37,10 @@ class Demon::Sidekiq < ::Demon::Base
|
|||||||
|
|
||||||
options = ["-c", GlobalSetting.sidekiq_workers.to_s]
|
options = ["-c", GlobalSetting.sidekiq_workers.to_s]
|
||||||
|
|
||||||
[['critical', 8], ['default', 4], ['low', 2], ['ultra_low', 1]].each do |queue_name, weight|
|
[["critical", 8], ["default", 4], ["low", 2], ["ultra_low", 1]].each do |queue_name, weight|
|
||||||
custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"]
|
custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"]
|
||||||
|
|
||||||
if !custom_queue_hostname || custom_queue_hostname.split(',').include?(Discourse.os_hostname)
|
if !custom_queue_hostname || custom_queue_hostname.split(",").include?(Discourse.os_hostname)
|
||||||
options << "-q"
|
options << "-q"
|
||||||
options << "#{queue_name},#{weight}"
|
options << "#{queue_name},#{weight}"
|
||||||
end
|
end
|
||||||
@ -49,7 +48,7 @@ class Demon::Sidekiq < ::Demon::Base
|
|||||||
|
|
||||||
# Sidekiq not as high priority as web, in this environment it is forked so a web is very
|
# Sidekiq not as high priority as web, in this environment it is forked so a web is very
|
||||||
# likely running
|
# likely running
|
||||||
Discourse::Utils.execute_command('renice', '-n', '5', '-p', Process.pid.to_s)
|
Discourse::Utils.execute_command("renice", "-n", "5", "-p", Process.pid.to_s)
|
||||||
|
|
||||||
cli.parse(options)
|
cli.parse(options)
|
||||||
load Rails.root + "config/initializers/100-sidekiq.rb"
|
load Rails.root + "config/initializers/100-sidekiq.rb"
|
||||||
@ -59,5 +58,4 @@ class Demon::Sidekiq < ::Demon::Base
|
|||||||
STDERR.puts e.backtrace.join("\n")
|
STDERR.puts e.backtrace.join("\n")
|
||||||
exit 1
|
exit 1
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,24 +1,23 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DirectoryHelper
|
module DirectoryHelper
|
||||||
|
|
||||||
def tmp_directory(prefix)
|
def tmp_directory(prefix)
|
||||||
directory_cache[prefix] ||= begin
|
directory_cache[prefix] ||= begin
|
||||||
f = File.join(Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S"))
|
f = File.join(Rails.root, "tmp", Time.now.strftime("#{prefix}%Y%m%d%H%M%S"))
|
||||||
FileUtils.mkdir_p(f) unless Dir[f].present?
|
FileUtils.mkdir_p(f) unless Dir[f].present?
|
||||||
f
|
f
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_tmp_directory(prefix)
|
def remove_tmp_directory(prefix)
|
||||||
tmp_directory_name = directory_cache[prefix] || ''
|
tmp_directory_name = directory_cache[prefix] || ""
|
||||||
directory_cache.delete(prefix)
|
directory_cache.delete(prefix)
|
||||||
FileUtils.rm_rf(tmp_directory_name) if Dir[tmp_directory_name].present?
|
FileUtils.rm_rf(tmp_directory_name) if Dir[tmp_directory_name].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def directory_cache
|
def directory_cache
|
||||||
@directory_cache ||= {}
|
@directory_cache ||= {}
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
452
lib/discourse.rb
452
lib/discourse.rb
@ -1,16 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'cache'
|
require "cache"
|
||||||
require 'open3'
|
require "open3"
|
||||||
require 'plugin/instance'
|
require "plugin/instance"
|
||||||
require 'version'
|
require "version"
|
||||||
|
|
||||||
module Discourse
|
module Discourse
|
||||||
DB_POST_MIGRATE_PATH ||= "db/post_migrate"
|
DB_POST_MIGRATE_PATH ||= "db/post_migrate"
|
||||||
REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME"
|
REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME"
|
||||||
|
|
||||||
class Utils
|
class Utils
|
||||||
URI_REGEXP ||= URI.regexp(%w{http https})
|
URI_REGEXP ||= URI.regexp(%w[http https])
|
||||||
|
|
||||||
# Usage:
|
# Usage:
|
||||||
# Discourse::Utils.execute_command("pwd", chdir: 'mydirectory')
|
# Discourse::Utils.execute_command("pwd", chdir: 'mydirectory')
|
||||||
@ -22,7 +22,9 @@ module Discourse
|
|||||||
runner = CommandRunner.new(**args)
|
runner = CommandRunner.new(**args)
|
||||||
|
|
||||||
if block_given?
|
if block_given?
|
||||||
raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present?
|
if command.present?
|
||||||
|
raise RuntimeError.new("Cannot pass command and block to execute_command")
|
||||||
|
end
|
||||||
yield runner
|
yield runner
|
||||||
else
|
else
|
||||||
runner.exec(*command)
|
runner.exec(*command)
|
||||||
@ -33,33 +35,32 @@ module Discourse
|
|||||||
logs.join("\n")
|
logs.join("\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.logs_markdown(logs, user:, filename: 'log.txt')
|
def self.logs_markdown(logs, user:, filename: "log.txt")
|
||||||
# Reserve 250 characters for the rest of the text
|
# Reserve 250 characters for the rest of the text
|
||||||
max_logs_length = SiteSetting.max_post_length - 250
|
max_logs_length = SiteSetting.max_post_length - 250
|
||||||
pretty_logs = Discourse::Utils.pretty_logs(logs)
|
pretty_logs = Discourse::Utils.pretty_logs(logs)
|
||||||
|
|
||||||
# If logs are short, try to inline them
|
# If logs are short, try to inline them
|
||||||
if pretty_logs.size < max_logs_length
|
return <<~TEXT if pretty_logs.size < max_logs_length
|
||||||
return <<~TEXT
|
|
||||||
```text
|
```text
|
||||||
#{pretty_logs}
|
#{pretty_logs}
|
||||||
```
|
```
|
||||||
TEXT
|
TEXT
|
||||||
end
|
|
||||||
|
|
||||||
# Try to create an upload for the logs
|
# Try to create an upload for the logs
|
||||||
upload = Dir.mktmpdir do |dir|
|
upload =
|
||||||
File.write(File.join(dir, filename), pretty_logs)
|
Dir.mktmpdir do |dir|
|
||||||
zipfile = Compression::Zip.new.compress(dir, filename)
|
File.write(File.join(dir, filename), pretty_logs)
|
||||||
File.open(zipfile) do |file|
|
zipfile = Compression::Zip.new.compress(dir, filename)
|
||||||
UploadCreator.new(
|
File.open(zipfile) do |file|
|
||||||
file,
|
UploadCreator.new(
|
||||||
File.basename(zipfile),
|
file,
|
||||||
type: 'backup_logs',
|
File.basename(zipfile),
|
||||||
for_export: 'true'
|
type: "backup_logs",
|
||||||
).create_for(user.id)
|
for_export: "true",
|
||||||
|
).create_for(user.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
if upload.persisted?
|
if upload.persisted?
|
||||||
return UploadMarkdown.new(upload).attachment_markdown
|
return UploadMarkdown.new(upload).attachment_markdown
|
||||||
@ -82,8 +83,8 @@ module Discourse
|
|||||||
rescue Errno::ENOENT
|
rescue Errno::ENOENT
|
||||||
end
|
end
|
||||||
|
|
||||||
FileUtils.mkdir_p(File.join(Rails.root, 'tmp'))
|
FileUtils.mkdir_p(File.join(Rails.root, "tmp"))
|
||||||
temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex)
|
temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex)
|
||||||
|
|
||||||
File.open(temp_destination, "w") do |fd|
|
File.open(temp_destination, "w") do |fd|
|
||||||
fd.write(contents)
|
fd.write(contents)
|
||||||
@ -101,9 +102,9 @@ module Discourse
|
|||||||
rescue Errno::ENOENT, Errno::EINVAL
|
rescue Errno::ENOENT, Errno::EINVAL
|
||||||
end
|
end
|
||||||
|
|
||||||
FileUtils.mkdir_p(File.join(Rails.root, 'tmp'))
|
FileUtils.mkdir_p(File.join(Rails.root, "tmp"))
|
||||||
temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex)
|
temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex)
|
||||||
execute_command('ln', '-s', source, temp_destination)
|
execute_command("ln", "-s", source, temp_destination)
|
||||||
FileUtils.mv(temp_destination, destination)
|
FileUtils.mv(temp_destination, destination)
|
||||||
|
|
||||||
nil
|
nil
|
||||||
@ -127,13 +128,22 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def exec(*command, **exec_params)
|
def exec(*command, **exec_params)
|
||||||
raise RuntimeError.new("Cannot specify same parameters at block and command level") if (@init_params.keys & exec_params.keys).present?
|
if (@init_params.keys & exec_params.keys).present?
|
||||||
|
raise RuntimeError.new("Cannot specify same parameters at block and command level")
|
||||||
|
end
|
||||||
execute_command(*command, **@init_params.merge(exec_params))
|
execute_command(*command, **@init_params.merge(exec_params))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def execute_command(*command, timeout: nil, failure_message: "", success_status_codes: [0], chdir: ".", unsafe_shell: false)
|
def execute_command(
|
||||||
|
*command,
|
||||||
|
timeout: nil,
|
||||||
|
failure_message: "",
|
||||||
|
success_status_codes: [0],
|
||||||
|
chdir: ".",
|
||||||
|
unsafe_shell: false
|
||||||
|
)
|
||||||
env = nil
|
env = nil
|
||||||
env = command.shift if command[0].is_a?(Hash)
|
env = command.shift if command[0].is_a?(Hash)
|
||||||
|
|
||||||
@ -156,11 +166,11 @@ module Discourse
|
|||||||
if !status.exited? || !success_status_codes.include?(status.exitstatus)
|
if !status.exited? || !success_status_codes.include?(status.exitstatus)
|
||||||
failure_message = "#{failure_message}\n" if !failure_message.blank?
|
failure_message = "#{failure_message}\n" if !failure_message.blank?
|
||||||
raise CommandError.new(
|
raise CommandError.new(
|
||||||
"#{caller[0]}: #{failure_message}#{stderr}",
|
"#{caller[0]}: #{failure_message}#{stderr}",
|
||||||
stdout: stdout,
|
stdout: stdout,
|
||||||
stderr: stderr,
|
stderr: stderr,
|
||||||
status: status
|
status: status,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
stdout
|
stdout
|
||||||
@ -195,33 +205,32 @@ module Discourse
|
|||||||
# mini_scheduler direct reporting
|
# mini_scheduler direct reporting
|
||||||
if Hash === job
|
if Hash === job
|
||||||
job_class = job["class"]
|
job_class = job["class"]
|
||||||
if job_class
|
job_exception_stats[job_class] += 1 if job_class
|
||||||
job_exception_stats[job_class] += 1
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# internal reporting
|
# internal reporting
|
||||||
if job.class == Class && ::Jobs::Base > job
|
job_exception_stats[job] += 1 if job.class == Class && ::Jobs::Base > job
|
||||||
job_exception_stats[job] += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
cm = RailsMultisite::ConnectionManagement
|
cm = RailsMultisite::ConnectionManagement
|
||||||
parent_logger.handle_exception(ex, {
|
parent_logger.handle_exception(
|
||||||
current_db: cm.current_db,
|
ex,
|
||||||
current_hostname: cm.current_hostname
|
{ current_db: cm.current_db, current_hostname: cm.current_hostname }.merge(context),
|
||||||
}.merge(context))
|
)
|
||||||
|
|
||||||
raise ex if Rails.env.test?
|
raise ex if Rails.env.test?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Expected less matches than what we got in a find
|
# Expected less matches than what we got in a find
|
||||||
class TooManyMatches < StandardError; end
|
class TooManyMatches < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# When they try to do something they should be logged in for
|
# When they try to do something they should be logged in for
|
||||||
class NotLoggedIn < StandardError; end
|
class NotLoggedIn < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# When the input is somehow bad
|
# When the input is somehow bad
|
||||||
class InvalidParameters < StandardError; end
|
class InvalidParameters < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# When they don't have permission to do something
|
# When they don't have permission to do something
|
||||||
class InvalidAccess < StandardError
|
class InvalidAccess < StandardError
|
||||||
@ -249,7 +258,13 @@ module Discourse
|
|||||||
attr_reader :original_path
|
attr_reader :original_path
|
||||||
attr_reader :custom_message
|
attr_reader :custom_message
|
||||||
|
|
||||||
def initialize(msg = nil, status: 404, check_permalinks: false, original_path: nil, custom_message: nil)
|
def initialize(
|
||||||
|
msg = nil,
|
||||||
|
status: 404,
|
||||||
|
check_permalinks: false,
|
||||||
|
original_path: nil,
|
||||||
|
custom_message: nil
|
||||||
|
)
|
||||||
super(msg)
|
super(msg)
|
||||||
|
|
||||||
@status = status
|
@status = status
|
||||||
@ -260,27 +275,33 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
# When a setting is missing
|
# When a setting is missing
|
||||||
class SiteSettingMissing < StandardError; end
|
class SiteSettingMissing < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# When ImageMagick is missing
|
# When ImageMagick is missing
|
||||||
class ImageMagickMissing < StandardError; end
|
class ImageMagickMissing < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# When read-only mode is enabled
|
# When read-only mode is enabled
|
||||||
class ReadOnly < StandardError; end
|
class ReadOnly < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
# Cross site request forgery
|
# Cross site request forgery
|
||||||
class CSRF < StandardError; end
|
class CSRF < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
class Deprecation < StandardError; end
|
class Deprecation < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
class ScssError < StandardError; end
|
class ScssError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
def self.filters
|
def self.filters
|
||||||
@filters ||= [:latest, :unread, :new, :unseen, :top, :read, :posted, :bookmarks]
|
@filters ||= %i[latest unread new unseen top read posted bookmarks]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.anonymous_filters
|
def self.anonymous_filters
|
||||||
@anonymous_filters ||= [:latest, :top, :categories]
|
@anonymous_filters ||= %i[latest top categories]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.top_menu_items
|
def self.top_menu_items
|
||||||
@ -288,7 +309,7 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.anonymous_top_menu_items
|
def self.anonymous_top_menu_items
|
||||||
@anonymous_top_menu_items ||= Discourse.anonymous_filters + [:categories, :top]
|
@anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top]
|
||||||
end
|
end
|
||||||
|
|
||||||
PIXEL_RATIOS ||= [1, 1.5, 2, 3]
|
PIXEL_RATIOS ||= [1, 1.5, 2, 3]
|
||||||
@ -297,26 +318,28 @@ module Discourse
|
|||||||
# TODO: should cache these when we get a notification system for site settings
|
# TODO: should cache these when we get a notification system for site settings
|
||||||
set = Set.new
|
set = Set.new
|
||||||
|
|
||||||
SiteSetting.avatar_sizes.split("|").map(&:to_i).each do |size|
|
SiteSetting
|
||||||
PIXEL_RATIOS.each do |pixel_ratio|
|
.avatar_sizes
|
||||||
set << (size * pixel_ratio).to_i
|
.split("|")
|
||||||
end
|
.map(&:to_i)
|
||||||
end
|
.each { |size| PIXEL_RATIOS.each { |pixel_ratio| set << (size * pixel_ratio).to_i } }
|
||||||
|
|
||||||
set
|
set
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.activate_plugins!
|
def self.activate_plugins!
|
||||||
@plugins = []
|
@plugins = []
|
||||||
Plugin::Instance.find_all("#{Rails.root}/plugins").each do |p|
|
Plugin::Instance
|
||||||
v = p.metadata.required_version || Discourse::VERSION::STRING
|
.find_all("#{Rails.root}/plugins")
|
||||||
if Discourse.has_needed_version?(Discourse::VERSION::STRING, v)
|
.each do |p|
|
||||||
p.activate!
|
v = p.metadata.required_version || Discourse::VERSION::STRING
|
||||||
@plugins << p
|
if Discourse.has_needed_version?(Discourse::VERSION::STRING, v)
|
||||||
else
|
p.activate!
|
||||||
STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})"
|
@plugins << p
|
||||||
|
else
|
||||||
|
STDERR.puts "Could not activate #{p.metadata.name}, discourse does not meet required version (#{v})"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
DiscourseEvent.trigger(:after_plugin_activation)
|
DiscourseEvent.trigger(:after_plugin_activation)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -360,9 +383,7 @@ module Discourse
|
|||||||
|
|
||||||
def self.apply_asset_filters(plugins, type, request)
|
def self.apply_asset_filters(plugins, type, request)
|
||||||
filter_opts = asset_filter_options(type, request)
|
filter_opts = asset_filter_options(type, request)
|
||||||
plugins.select do |plugin|
|
plugins.select { |plugin| plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } }
|
||||||
plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.asset_filter_options(type, request)
|
def self.asset_filter_options(type, request)
|
||||||
@ -385,20 +406,24 @@ module Discourse
|
|||||||
targets << :desktop if args[:desktop_view]
|
targets << :desktop if args[:desktop_view]
|
||||||
|
|
||||||
targets.each do |target|
|
targets.each do |target|
|
||||||
assets += plugins.find_all do |plugin|
|
assets +=
|
||||||
plugin.css_asset_exists?(target)
|
plugins
|
||||||
end.map do |plugin|
|
.find_all { |plugin| plugin.css_asset_exists?(target) }
|
||||||
target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}"
|
.map do |plugin|
|
||||||
end
|
target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
assets
|
assets
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.find_plugin_js_assets(args)
|
def self.find_plugin_js_assets(args)
|
||||||
plugins = self.find_plugins(args).select do |plugin|
|
plugins =
|
||||||
plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists?
|
self
|
||||||
end
|
.find_plugins(args)
|
||||||
|
.select do |plugin|
|
||||||
|
plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists?
|
||||||
|
end
|
||||||
|
|
||||||
plugins = apply_asset_filters(plugins, :js, args[:request])
|
plugins = apply_asset_filters(plugins, :js, args[:request])
|
||||||
|
|
||||||
@ -413,25 +438,33 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.assets_digest
|
def self.assets_digest
|
||||||
@assets_digest ||= begin
|
@assets_digest ||=
|
||||||
digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join)
|
begin
|
||||||
|
digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join)
|
||||||
|
|
||||||
channel = "/global/asset-version"
|
channel = "/global/asset-version"
|
||||||
message = MessageBus.last_message(channel)
|
message = MessageBus.last_message(channel)
|
||||||
|
|
||||||
unless message && message.data == digest
|
MessageBus.publish channel, digest unless message && message.data == digest
|
||||||
MessageBus.publish channel, digest
|
digest
|
||||||
end
|
end
|
||||||
digest
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
BUILTIN_AUTH ||= [
|
BUILTIN_AUTH ||= [
|
||||||
Auth::AuthProvider.new(authenticator: Auth::FacebookAuthenticator.new, frame_width: 580, frame_height: 400, icon: "fab-facebook"),
|
Auth::AuthProvider.new(
|
||||||
Auth::AuthProvider.new(authenticator: Auth::GoogleOAuth2Authenticator.new, frame_width: 850, frame_height: 500), # Custom icon implemented in client
|
authenticator: Auth::FacebookAuthenticator.new,
|
||||||
|
frame_width: 580,
|
||||||
|
frame_height: 400,
|
||||||
|
icon: "fab-facebook",
|
||||||
|
),
|
||||||
|
Auth::AuthProvider.new(
|
||||||
|
authenticator: Auth::GoogleOAuth2Authenticator.new,
|
||||||
|
frame_width: 850,
|
||||||
|
frame_height: 500,
|
||||||
|
), # Custom icon implemented in client
|
||||||
Auth::AuthProvider.new(authenticator: Auth::GithubAuthenticator.new, icon: "fab-github"),
|
Auth::AuthProvider.new(authenticator: Auth::GithubAuthenticator.new, icon: "fab-github"),
|
||||||
Auth::AuthProvider.new(authenticator: Auth::TwitterAuthenticator.new, icon: "fab-twitter"),
|
Auth::AuthProvider.new(authenticator: Auth::TwitterAuthenticator.new, icon: "fab-twitter"),
|
||||||
Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord")
|
Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord"),
|
||||||
]
|
]
|
||||||
|
|
||||||
def self.auth_providers
|
def self.auth_providers
|
||||||
@ -439,7 +472,7 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.enabled_auth_providers
|
def self.enabled_auth_providers
|
||||||
auth_providers.select { |provider| provider.authenticator.enabled? }
|
auth_providers.select { |provider| provider.authenticator.enabled? }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.authenticators
|
def self.authenticators
|
||||||
@ -449,17 +482,18 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.enabled_authenticators
|
def self.enabled_authenticators
|
||||||
authenticators.select { |authenticator| authenticator.enabled? }
|
authenticators.select { |authenticator| authenticator.enabled? }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cache
|
def self.cache
|
||||||
@cache ||= begin
|
@cache ||=
|
||||||
if GlobalSetting.skip_redis?
|
begin
|
||||||
ActiveSupport::Cache::MemoryStore.new
|
if GlobalSetting.skip_redis?
|
||||||
else
|
ActiveSupport::Cache::MemoryStore.new
|
||||||
Cache.new
|
else
|
||||||
|
Cache.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# hostname of the server, operating system level
|
# hostname of the server, operating system level
|
||||||
@ -467,15 +501,15 @@ module Discourse
|
|||||||
def self.os_hostname
|
def self.os_hostname
|
||||||
@os_hostname ||=
|
@os_hostname ||=
|
||||||
begin
|
begin
|
||||||
require 'socket'
|
require "socket"
|
||||||
Socket.gethostname
|
Socket.gethostname
|
||||||
rescue => e
|
rescue => e
|
||||||
warn_exception(e, message: 'Socket.gethostname is not working')
|
warn_exception(e, message: "Socket.gethostname is not working")
|
||||||
begin
|
begin
|
||||||
`hostname`.strip
|
`hostname`.strip
|
||||||
rescue => e
|
rescue => e
|
||||||
warn_exception(e, message: 'hostname command is not working')
|
warn_exception(e, message: "hostname command is not working")
|
||||||
'unknown_host'
|
"unknown_host"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -501,12 +535,12 @@ module Discourse
|
|||||||
def self.current_hostname_with_port
|
def self.current_hostname_with_port
|
||||||
default_port = SiteSetting.force_https? ? 443 : 80
|
default_port = SiteSetting.force_https? ? 443 : 80
|
||||||
result = +"#{current_hostname}"
|
result = +"#{current_hostname}"
|
||||||
result << ":#{SiteSetting.port}" if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port
|
if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port
|
||||||
|
result << ":#{SiteSetting.port}"
|
||||||
if Rails.env.development? && SiteSetting.port.blank?
|
|
||||||
result << ":#{ENV["UNICORN_PORT"] || 3000}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
result << ":#{ENV["UNICORN_PORT"] || 3000}" if Rails.env.development? && SiteSetting.port.blank?
|
||||||
|
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -520,16 +554,18 @@ module Discourse
|
|||||||
|
|
||||||
def self.route_for(uri)
|
def self.route_for(uri)
|
||||||
unless uri.is_a?(URI)
|
unless uri.is_a?(URI)
|
||||||
uri = begin
|
uri =
|
||||||
URI(uri)
|
begin
|
||||||
rescue ArgumentError, URI::Error
|
URI(uri)
|
||||||
end
|
rescue ArgumentError, URI::Error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return unless uri
|
return unless uri
|
||||||
|
|
||||||
path = +(uri.path || "")
|
path = +(uri.path || "")
|
||||||
if !uri.host || (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path))
|
if !uri.host ||
|
||||||
|
(uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path))
|
||||||
path.slice!(Discourse.base_path)
|
path.slice!(Discourse.base_path)
|
||||||
return Rails.application.routes.recognize_path(path)
|
return Rails.application.routes.recognize_path(path)
|
||||||
end
|
end
|
||||||
@ -543,21 +579,21 @@ module Discourse
|
|||||||
alias_method :base_url_no_path, :base_url_no_prefix
|
alias_method :base_url_no_path, :base_url_no_prefix
|
||||||
end
|
end
|
||||||
|
|
||||||
READONLY_MODE_KEY_TTL ||= 60
|
READONLY_MODE_KEY_TTL ||= 60
|
||||||
READONLY_MODE_KEY ||= 'readonly_mode'
|
READONLY_MODE_KEY ||= "readonly_mode"
|
||||||
PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres'
|
PG_READONLY_MODE_KEY ||= "readonly_mode:postgres"
|
||||||
PG_READONLY_MODE_KEY_TTL ||= 300
|
PG_READONLY_MODE_KEY_TTL ||= 300
|
||||||
USER_READONLY_MODE_KEY ||= 'readonly_mode:user'
|
USER_READONLY_MODE_KEY ||= "readonly_mode:user"
|
||||||
PG_FORCE_READONLY_MODE_KEY ||= 'readonly_mode:postgres_force'
|
PG_FORCE_READONLY_MODE_KEY ||= "readonly_mode:postgres_force"
|
||||||
|
|
||||||
# Psuedo readonly mode, where staff can still write
|
# Psuedo readonly mode, where staff can still write
|
||||||
STAFF_WRITES_ONLY_MODE_KEY ||= 'readonly_mode:staff_writes_only'
|
STAFF_WRITES_ONLY_MODE_KEY ||= "readonly_mode:staff_writes_only"
|
||||||
|
|
||||||
READONLY_KEYS ||= [
|
READONLY_KEYS ||= [
|
||||||
READONLY_MODE_KEY,
|
READONLY_MODE_KEY,
|
||||||
PG_READONLY_MODE_KEY,
|
PG_READONLY_MODE_KEY,
|
||||||
USER_READONLY_MODE_KEY,
|
USER_READONLY_MODE_KEY,
|
||||||
PG_FORCE_READONLY_MODE_KEY
|
PG_FORCE_READONLY_MODE_KEY,
|
||||||
]
|
]
|
||||||
|
|
||||||
def self.enable_readonly_mode(key = READONLY_MODE_KEY)
|
def self.enable_readonly_mode(key = READONLY_MODE_KEY)
|
||||||
@ -565,7 +601,9 @@ module Discourse
|
|||||||
Sidekiq.pause!("pg_failover") if !Sidekiq.paused?
|
Sidekiq.pause!("pg_failover") if !Sidekiq.paused?
|
||||||
end
|
end
|
||||||
|
|
||||||
if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?(key)
|
if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?(
|
||||||
|
key,
|
||||||
|
)
|
||||||
Discourse.redis.set(key, 1)
|
Discourse.redis.set(key, 1)
|
||||||
else
|
else
|
||||||
ttl =
|
ttl =
|
||||||
@ -595,15 +633,13 @@ module Discourse
|
|||||||
|
|
||||||
unless @threads[key]&.alive?
|
unless @threads[key]&.alive?
|
||||||
@threads[key] = Thread.new do
|
@threads[key] = Thread.new do
|
||||||
while @dbs.size > 0 do
|
while @dbs.size > 0
|
||||||
sleep ttl / 2
|
sleep ttl / 2
|
||||||
|
|
||||||
@mutex.synchronize do
|
@mutex.synchronize do
|
||||||
@dbs.each do |db|
|
@dbs.each do |db|
|
||||||
RailsMultisite::ConnectionManagement.with_connection(db) do
|
RailsMultisite::ConnectionManagement.with_connection(db) do
|
||||||
if !Discourse.redis.expire(key, ttl)
|
@dbs.delete(db) if !Discourse.redis.expire(key, ttl)
|
||||||
@dbs.delete(db)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -653,7 +689,7 @@ module Discourse
|
|||||||
|
|
||||||
# Shared between processes
|
# Shared between processes
|
||||||
def self.postgres_last_read_only
|
def self.postgres_last_read_only
|
||||||
@postgres_last_read_only ||= DistributedCache.new('postgres_last_read_only', namespace: false)
|
@postgres_last_read_only ||= DistributedCache.new("postgres_last_read_only", namespace: false)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Per-process
|
# Per-process
|
||||||
@ -698,39 +734,43 @@ module Discourse
|
|||||||
# This is better than `MessageBus.publish "/file-change", ["refresh"]` because
|
# This is better than `MessageBus.publish "/file-change", ["refresh"]` because
|
||||||
# it spreads the refreshes out over a time period
|
# it spreads the refreshes out over a time period
|
||||||
if user_ids
|
if user_ids
|
||||||
MessageBus.publish("/refresh_client", 'clobber', user_ids: user_ids)
|
MessageBus.publish("/refresh_client", "clobber", user_ids: user_ids)
|
||||||
else
|
else
|
||||||
MessageBus.publish('/global/asset-version', 'clobber')
|
MessageBus.publish("/global/asset-version", "clobber")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.git_version
|
def self.git_version
|
||||||
@git_version ||= begin
|
@git_version ||=
|
||||||
git_cmd = 'git rev-parse HEAD'
|
begin
|
||||||
self.try_git(git_cmd, Discourse::VERSION::STRING)
|
git_cmd = "git rev-parse HEAD"
|
||||||
end
|
self.try_git(git_cmd, Discourse::VERSION::STRING)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.git_branch
|
def self.git_branch
|
||||||
@git_branch ||= begin
|
@git_branch ||=
|
||||||
git_cmd = 'git rev-parse --abbrev-ref HEAD'
|
begin
|
||||||
self.try_git(git_cmd, 'unknown')
|
git_cmd = "git rev-parse --abbrev-ref HEAD"
|
||||||
end
|
self.try_git(git_cmd, "unknown")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.full_version
|
def self.full_version
|
||||||
@full_version ||= begin
|
@full_version ||=
|
||||||
git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null'
|
begin
|
||||||
self.try_git(git_cmd, 'unknown')
|
git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null'
|
||||||
end
|
self.try_git(git_cmd, "unknown")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.last_commit_date
|
def self.last_commit_date
|
||||||
@last_commit_date ||= begin
|
@last_commit_date ||=
|
||||||
git_cmd = 'git log -1 --format="%ct"'
|
begin
|
||||||
seconds = self.try_git(git_cmd, nil)
|
git_cmd = 'git log -1 --format="%ct"'
|
||||||
seconds.nil? ? nil : DateTime.strptime(seconds, '%s')
|
seconds = self.try_git(git_cmd, nil)
|
||||||
end
|
seconds.nil? ? nil : DateTime.strptime(seconds, "%s")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.try_git(git_cmd, default_value)
|
def self.try_git(git_cmd, default_value)
|
||||||
@ -738,20 +778,21 @@ module Discourse
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
version_value = `#{git_cmd}`.strip
|
version_value = `#{git_cmd}`.strip
|
||||||
rescue
|
rescue StandardError
|
||||||
version_value = default_value
|
version_value = default_value
|
||||||
end
|
end
|
||||||
|
|
||||||
if version_value.empty?
|
version_value = default_value if version_value.empty?
|
||||||
version_value = default_value
|
|
||||||
end
|
|
||||||
|
|
||||||
version_value
|
version_value
|
||||||
end
|
end
|
||||||
|
|
||||||
# Either returns the site_contact_username user or the first admin.
|
# Either returns the site_contact_username user or the first admin.
|
||||||
def self.site_contact_user
|
def self.site_contact_user
|
||||||
user = User.find_by(username_lower: SiteSetting.site_contact_username.downcase) if SiteSetting.site_contact_username.present?
|
user =
|
||||||
|
User.find_by(
|
||||||
|
username_lower: SiteSetting.site_contact_username.downcase,
|
||||||
|
) if SiteSetting.site_contact_username.present?
|
||||||
user ||= (system_user || User.admins.real.order(:id).first)
|
user ||= (system_user || User.admins.real.order(:id).first)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -765,10 +806,10 @@ module Discourse
|
|||||||
|
|
||||||
def self.store
|
def self.store
|
||||||
if SiteSetting.Upload.enable_s3_uploads
|
if SiteSetting.Upload.enable_s3_uploads
|
||||||
@s3_store_loaded ||= require 'file_store/s3_store'
|
@s3_store_loaded ||= require "file_store/s3_store"
|
||||||
FileStore::S3Store.new
|
FileStore::S3Store.new
|
||||||
else
|
else
|
||||||
@local_store_loaded ||= require 'file_store/local_store'
|
@local_store_loaded ||= require "file_store/local_store"
|
||||||
FileStore::LocalStore.new
|
FileStore::LocalStore.new
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -805,15 +846,15 @@ module Discourse
|
|||||||
Discourse.cache.reconnect
|
Discourse.cache.reconnect
|
||||||
Logster.store.redis.reconnect
|
Logster.store.redis.reconnect
|
||||||
# shuts down all connections in the pool
|
# shuts down all connections in the pool
|
||||||
Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! }
|
Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! }
|
||||||
# re-establish
|
# re-establish
|
||||||
Sidekiq.redis = sidekiq_redis_config
|
Sidekiq.redis = sidekiq_redis_config
|
||||||
|
|
||||||
# in case v8 was initialized we want to make sure it is nil
|
# in case v8 was initialized we want to make sure it is nil
|
||||||
PrettyText.reset_context
|
PrettyText.reset_context
|
||||||
|
|
||||||
DiscourseJsProcessor::Transpiler.reset_context if defined? DiscourseJsProcessor::Transpiler
|
DiscourseJsProcessor::Transpiler.reset_context if defined?(DiscourseJsProcessor::Transpiler)
|
||||||
JsLocaleHelper.reset_context if defined? JsLocaleHelper
|
JsLocaleHelper.reset_context if defined?(JsLocaleHelper)
|
||||||
|
|
||||||
# warm up v8 after fork, that way we do not fork a v8 context
|
# warm up v8 after fork, that way we do not fork a v8 context
|
||||||
# it may cause issues if bg threads in a v8 isolate randomly stop
|
# it may cause issues if bg threads in a v8 isolate randomly stop
|
||||||
@ -831,7 +872,7 @@ module Discourse
|
|||||||
# you can use Discourse.warn when you want to report custom environment
|
# you can use Discourse.warn when you want to report custom environment
|
||||||
# with the error, this helps with grouping
|
# with the error, this helps with grouping
|
||||||
def self.warn(message, env = nil)
|
def self.warn(message, env = nil)
|
||||||
append = env ? (+" ") << env.map { |k, v|"#{k}: #{v}" }.join(" ") : ""
|
append = env ? (+" ") << env.map { |k, v| "#{k}: #{v}" }.join(" ") : ""
|
||||||
|
|
||||||
if !(Logster::Logger === Rails.logger)
|
if !(Logster::Logger === Rails.logger)
|
||||||
Rails.logger.warn("#{message}#{append}")
|
Rails.logger.warn("#{message}#{append}")
|
||||||
@ -839,9 +880,7 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
loggers = [Rails.logger]
|
loggers = [Rails.logger]
|
||||||
if Rails.logger.chained
|
loggers.concat(Rails.logger.chained) if Rails.logger.chained
|
||||||
loggers.concat(Rails.logger.chained)
|
|
||||||
end
|
|
||||||
|
|
||||||
logster_env = env
|
logster_env = env
|
||||||
|
|
||||||
@ -849,9 +888,7 @@ module Discourse
|
|||||||
logster_env = Logster::Message.populate_from_env(old_env)
|
logster_env = Logster::Message.populate_from_env(old_env)
|
||||||
|
|
||||||
# a bit awkward by try to keep the new params
|
# a bit awkward by try to keep the new params
|
||||||
env.each do |k, v|
|
env.each { |k, v| logster_env[k] = v }
|
||||||
logster_env[k] = v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
loggers.each do |logger|
|
loggers.each do |logger|
|
||||||
@ -860,12 +897,7 @@ module Discourse
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.store.report(
|
logger.store.report(::Logger::Severity::WARN, "discourse", message, env: logster_env)
|
||||||
::Logger::Severity::WARN,
|
|
||||||
"discourse",
|
|
||||||
message,
|
|
||||||
env: logster_env
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if old_env
|
if old_env
|
||||||
@ -881,7 +913,6 @@ module Discourse
|
|||||||
# report a warning maintaining backtrack for logster
|
# report a warning maintaining backtrack for logster
|
||||||
def self.warn_exception(e, message: "", env: nil)
|
def self.warn_exception(e, message: "", env: nil)
|
||||||
if Rails.logger.respond_to? :add_with_opts
|
if Rails.logger.respond_to? :add_with_opts
|
||||||
|
|
||||||
env ||= {}
|
env ||= {}
|
||||||
env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db
|
env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db
|
||||||
|
|
||||||
@ -891,13 +922,13 @@ module Discourse
|
|||||||
"#{message} : #{e.class.name} : #{e}",
|
"#{message} : #{e.class.name} : #{e}",
|
||||||
"discourse-exception",
|
"discourse-exception",
|
||||||
backtrace: e.backtrace.join("\n"),
|
backtrace: e.backtrace.join("\n"),
|
||||||
env: env
|
env: env,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
# no logster ... fallback
|
# no logster ... fallback
|
||||||
Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}")
|
Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}")
|
||||||
end
|
end
|
||||||
rescue
|
rescue StandardError
|
||||||
STDERR.puts "Failed to report exception #{e} #{message}"
|
STDERR.puts "Failed to report exception #{e} #{message}"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -909,17 +940,11 @@ module Discourse
|
|||||||
warning << "\nAt #{location}"
|
warning << "\nAt #{location}"
|
||||||
warning = warning.join(" ")
|
warning = warning.join(" ")
|
||||||
|
|
||||||
if raise_error
|
raise Deprecation.new(warning) if raise_error
|
||||||
raise Deprecation.new(warning)
|
|
||||||
end
|
|
||||||
|
|
||||||
if Rails.env == "development"
|
STDERR.puts(warning) if Rails.env == "development"
|
||||||
STDERR.puts(warning)
|
|
||||||
end
|
|
||||||
|
|
||||||
if output_in_test && Rails.env == "test"
|
STDERR.puts(warning) if output_in_test && Rails.env == "test"
|
||||||
STDERR.puts(warning)
|
|
||||||
end
|
|
||||||
|
|
||||||
digest = Digest::MD5.hexdigest(warning)
|
digest = Digest::MD5.hexdigest(warning)
|
||||||
redis_key = "deprecate-notice-#{digest}"
|
redis_key = "deprecate-notice-#{digest}"
|
||||||
@ -935,7 +960,7 @@ module Discourse
|
|||||||
warning
|
warning
|
||||||
end
|
end
|
||||||
|
|
||||||
SIDEKIQ_NAMESPACE ||= 'sidekiq'
|
SIDEKIQ_NAMESPACE ||= "sidekiq"
|
||||||
|
|
||||||
def self.sidekiq_redis_config
|
def self.sidekiq_redis_config
|
||||||
conf = GlobalSetting.redis_config.dup
|
conf = GlobalSetting.redis_config.dup
|
||||||
@ -951,7 +976,8 @@ module Discourse
|
|||||||
|
|
||||||
def self.reset_active_record_cache_if_needed(e)
|
def self.reset_active_record_cache_if_needed(e)
|
||||||
last_cache_reset = Discourse.last_ar_cache_reset
|
last_cache_reset = Discourse.last_ar_cache_reset
|
||||||
if e && e.message =~ /UndefinedColumn/ && (last_cache_reset.nil? || last_cache_reset < 30.seconds.ago)
|
if e && e.message =~ /UndefinedColumn/ &&
|
||||||
|
(last_cache_reset.nil? || last_cache_reset < 30.seconds.ago)
|
||||||
Rails.logger.warn "Clearing Active Record cache, this can happen if schema changed while site is running or in a multisite various databases are running different schemas. Consider running rake multisite:migrate."
|
Rails.logger.warn "Clearing Active Record cache, this can happen if schema changed while site is running or in a multisite various databases are running different schemas. Consider running rake multisite:migrate."
|
||||||
Discourse.last_ar_cache_reset = Time.zone.now
|
Discourse.last_ar_cache_reset = Time.zone.now
|
||||||
Discourse.reset_active_record_cache
|
Discourse.reset_active_record_cache
|
||||||
@ -961,7 +987,11 @@ module Discourse
|
|||||||
def self.reset_active_record_cache
|
def self.reset_active_record_cache
|
||||||
ActiveRecord::Base.connection.query_cache.clear
|
ActiveRecord::Base.connection.query_cache.clear
|
||||||
(ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
|
(ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
|
||||||
table.classify.constantize.reset_column_information rescue nil
|
begin
|
||||||
|
table.classify.constantize.reset_column_information
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
@ -971,7 +1001,7 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.skip_post_deployment_migrations?
|
def self.skip_post_deployment_migrations?
|
||||||
['1', 'true'].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s)
|
%w[1 true].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# this is used to preload as much stuff as possible prior to forking
|
# this is used to preload as much stuff as possible prior to forking
|
||||||
@ -985,7 +1015,11 @@ module Discourse
|
|||||||
|
|
||||||
# load up all models and schema
|
# load up all models and schema
|
||||||
(ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
|
(ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
|
||||||
table.classify.constantize.first rescue nil
|
begin
|
||||||
|
table.classify.constantize.first
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# ensure we have a full schema cache in case we missed something above
|
# ensure we have a full schema cache in case we missed something above
|
||||||
@ -1024,29 +1058,27 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
Thread.new {
|
Thread.new do
|
||||||
# router warm up
|
# router warm up
|
||||||
Rails.application.routes.recognize_path('abc') rescue nil
|
begin
|
||||||
},
|
Rails.application.routes.recognize_path("abc")
|
||||||
Thread.new {
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
Thread.new do
|
||||||
# preload discourse version
|
# preload discourse version
|
||||||
Discourse.git_version
|
Discourse.git_version
|
||||||
Discourse.git_branch
|
Discourse.git_branch
|
||||||
Discourse.full_version
|
Discourse.full_version
|
||||||
},
|
end,
|
||||||
Thread.new {
|
Thread.new do
|
||||||
require 'actionview_precompiler'
|
require "actionview_precompiler"
|
||||||
ActionviewPrecompiler.precompile
|
ActionviewPrecompiler.precompile
|
||||||
},
|
end,
|
||||||
Thread.new {
|
Thread.new { LetterAvatar.image_magick_version },
|
||||||
LetterAvatar.image_magick_version
|
Thread.new { SvgSprite.core_svgs },
|
||||||
},
|
Thread.new { EmberCli.script_chunks },
|
||||||
Thread.new {
|
|
||||||
SvgSprite.core_svgs
|
|
||||||
},
|
|
||||||
Thread.new {
|
|
||||||
EmberCli.script_chunks
|
|
||||||
}
|
|
||||||
].each(&:join)
|
].each(&:join)
|
||||||
ensure
|
ensure
|
||||||
@preloaded_rails = true
|
@preloaded_rails = true
|
||||||
@ -1055,10 +1087,10 @@ module Discourse
|
|||||||
mattr_accessor :redis
|
mattr_accessor :redis
|
||||||
|
|
||||||
def self.is_parallel_test?
|
def self.is_parallel_test?
|
||||||
ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER']
|
ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"]
|
||||||
end
|
end
|
||||||
|
|
||||||
CDN_REQUEST_METHODS ||= ["GET", "HEAD", "OPTIONS"]
|
CDN_REQUEST_METHODS ||= %w[GET HEAD OPTIONS]
|
||||||
|
|
||||||
def self.is_cdn_request?(env, request_method)
|
def self.is_cdn_request?(env, request_method)
|
||||||
return unless CDN_REQUEST_METHODS.include?(request_method)
|
return unless CDN_REQUEST_METHODS.include?(request_method)
|
||||||
@ -1071,8 +1103,8 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.apply_cdn_headers(headers)
|
def self.apply_cdn_headers(headers)
|
||||||
headers['Access-Control-Allow-Origin'] = '*'
|
headers["Access-Control-Allow-Origin"] = "*"
|
||||||
headers['Access-Control-Allow-Methods'] = CDN_REQUEST_METHODS.join(", ")
|
headers["Access-Control-Allow-Methods"] = CDN_REQUEST_METHODS.join(", ")
|
||||||
headers
|
headers
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1091,8 +1123,12 @@ module Discourse
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.anonymous_locale(request)
|
def self.anonymous_locale(request)
|
||||||
locale = HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie
|
locale =
|
||||||
locale ||= HttpLanguageParser.parse(request.env["HTTP_ACCEPT_LANGUAGE"]) if SiteSetting.set_locale_from_accept_language_header
|
HttpLanguageParser.parse(request.cookies["locale"]) if SiteSetting.set_locale_from_cookie
|
||||||
|
locale ||=
|
||||||
|
HttpLanguageParser.parse(
|
||||||
|
request.env["HTTP_ACCEPT_LANGUAGE"],
|
||||||
|
) if SiteSetting.set_locale_from_accept_language_header
|
||||||
locale
|
locale
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiscourseConnectBase
|
class DiscourseConnectBase
|
||||||
|
class ParseError < RuntimeError
|
||||||
|
end
|
||||||
|
|
||||||
class ParseError < RuntimeError; end
|
ACCESSORS = %i[
|
||||||
|
|
||||||
ACCESSORS = %i{
|
|
||||||
add_groups
|
add_groups
|
||||||
admin moderator
|
admin
|
||||||
|
moderator
|
||||||
avatar_force_update
|
avatar_force_update
|
||||||
avatar_url
|
avatar_url
|
||||||
bio
|
bio
|
||||||
@ -31,11 +32,11 @@ class DiscourseConnectBase
|
|||||||
title
|
title
|
||||||
username
|
username
|
||||||
website
|
website
|
||||||
}
|
]
|
||||||
|
|
||||||
FIXNUMS = []
|
FIXNUMS = []
|
||||||
|
|
||||||
BOOLS = %i{
|
BOOLS = %i[
|
||||||
admin
|
admin
|
||||||
avatar_force_update
|
avatar_force_update
|
||||||
confirmed_2fa
|
confirmed_2fa
|
||||||
@ -46,7 +47,7 @@ class DiscourseConnectBase
|
|||||||
require_2fa
|
require_2fa
|
||||||
require_activation
|
require_activation
|
||||||
suppress_welcome_message
|
suppress_welcome_message
|
||||||
}
|
]
|
||||||
|
|
||||||
def self.nonce_expiry_time
|
def self.nonce_expiry_time
|
||||||
@nonce_expiry_time ||= 10.minutes
|
@nonce_expiry_time ||= 10.minutes
|
||||||
@ -80,9 +81,11 @@ class DiscourseConnectBase
|
|||||||
decoded_hash = Rack::Utils.parse_query(decoded)
|
decoded_hash = Rack::Utils.parse_query(decoded)
|
||||||
|
|
||||||
if sso.sign(parsed["sso"]) != parsed["sig"]
|
if sso.sign(parsed["sso"]) != parsed["sig"]
|
||||||
diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}"
|
diags =
|
||||||
if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m
|
"\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}"
|
||||||
raise ParseError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}"
|
if parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m
|
||||||
|
raise ParseError,
|
||||||
|
"The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}"
|
||||||
else
|
else
|
||||||
raise ParseError, "Bad signature for payload #{diags}"
|
raise ParseError, "Bad signature for payload #{diags}"
|
||||||
end
|
end
|
||||||
@ -91,9 +94,7 @@ class DiscourseConnectBase
|
|||||||
ACCESSORS.each do |k|
|
ACCESSORS.each do |k|
|
||||||
val = decoded_hash[k.to_s]
|
val = decoded_hash[k.to_s]
|
||||||
val = val.to_i if FIXNUMS.include? k
|
val = val.to_i if FIXNUMS.include? k
|
||||||
if BOOLS.include? k
|
val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k
|
||||||
val = ["true", "false"].include?(val) ? val == "true" : nil
|
|
||||||
end
|
|
||||||
sso.public_send("#{k}=", val)
|
sso.public_send("#{k}=", val)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -137,12 +138,12 @@ class DiscourseConnectBase
|
|||||||
|
|
||||||
def to_url(base_url = nil)
|
def to_url(base_url = nil)
|
||||||
base = "#{base_url || sso_url}"
|
base = "#{base_url || sso_url}"
|
||||||
"#{base}#{base.include?('?') ? '&' : '?'}#{payload}"
|
"#{base}#{base.include?("?") ? "&" : "?"}#{payload}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def payload(secret = nil)
|
def payload(secret = nil)
|
||||||
payload = Base64.strict_encode64(unsigned_payload)
|
payload = Base64.strict_encode64(unsigned_payload)
|
||||||
"sso=#{CGI::escape(payload)}&sig=#{sign(payload, secret)}"
|
"sso=#{CGI.escape(payload)}&sig=#{sign(payload, secret)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def unsigned_payload
|
def unsigned_payload
|
||||||
@ -157,9 +158,7 @@ class DiscourseConnectBase
|
|||||||
payload[k] = val
|
payload[k] = val
|
||||||
end
|
end
|
||||||
|
|
||||||
@custom_fields&.each do |k, v|
|
@custom_fields&.each { |k, v| payload["custom.#{k}"] = v.to_s }
|
||||||
payload["custom.#{k}"] = v.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
payload
|
payload
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiscourseConnectProvider < DiscourseConnectBase
|
class DiscourseConnectProvider < DiscourseConnectBase
|
||||||
class BlankSecret < RuntimeError; end
|
class BlankSecret < RuntimeError
|
||||||
class BlankReturnUrl < RuntimeError; end
|
end
|
||||||
|
class BlankReturnUrl < RuntimeError
|
||||||
|
end
|
||||||
|
|
||||||
def self.parse(payload, sso_secret = nil, **init_kwargs)
|
def self.parse(payload, sso_secret = nil, **init_kwargs)
|
||||||
parsed_payload = Rack::Utils.parse_query(payload)
|
parsed_payload = Rack::Utils.parse_query(payload)
|
||||||
@ -15,11 +17,16 @@ class DiscourseConnectProvider < DiscourseConnectBase
|
|||||||
if sso_secret.blank?
|
if sso_secret.blank?
|
||||||
begin
|
begin
|
||||||
host = URI.parse(return_sso_url).host
|
host = URI.parse(return_sso_url).host
|
||||||
Rails.logger.warn("SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings")
|
Rails.logger.warn(
|
||||||
|
"SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings",
|
||||||
|
)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
# going for StandardError cause URI::Error may not be enough, eg it parses to something not
|
# going for StandardError cause URI::Error may not be enough, eg it parses to something not
|
||||||
# responding to host
|
# responding to host
|
||||||
Discourse.warn_exception(e, message: "SSO failed; invalid or missing return_sso_url in SSO payload")
|
Discourse.warn_exception(
|
||||||
|
e,
|
||||||
|
message: "SSO failed; invalid or missing return_sso_url in SSO payload",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise BlankSecret
|
raise BlankSecret
|
||||||
@ -31,7 +38,7 @@ class DiscourseConnectProvider < DiscourseConnectBase
|
|||||||
def self.lookup_return_sso_url(parsed_payload)
|
def self.lookup_return_sso_url(parsed_payload)
|
||||||
decoded = Base64.decode64(parsed_payload["sso"])
|
decoded = Base64.decode64(parsed_payload["sso"])
|
||||||
decoded_hash = Rack::Utils.parse_query(decoded)
|
decoded_hash = Rack::Utils.parse_query(decoded)
|
||||||
decoded_hash['return_sso_url']
|
decoded_hash["return_sso_url"]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.lookup_sso_secret(return_sso_url, parsed_payload)
|
def self.lookup_sso_secret(return_sso_url, parsed_payload)
|
||||||
@ -39,21 +46,23 @@ class DiscourseConnectProvider < DiscourseConnectBase
|
|||||||
|
|
||||||
return_url_host = URI.parse(return_sso_url).host
|
return_url_host = URI.parse(return_sso_url).host
|
||||||
|
|
||||||
provider_secrets = SiteSetting
|
provider_secrets =
|
||||||
.discourse_connect_provider_secrets
|
SiteSetting
|
||||||
.split("\n")
|
.discourse_connect_provider_secrets
|
||||||
.map { |row| row.split("|", 2) }
|
.split("\n")
|
||||||
.sort_by { |k, _| k }
|
.map { |row| row.split("|", 2) }
|
||||||
.reverse
|
.sort_by { |k, _| k }
|
||||||
|
.reverse
|
||||||
|
|
||||||
first_domain_match = nil
|
first_domain_match = nil
|
||||||
|
|
||||||
pair = provider_secrets.find do |domain, configured_secret|
|
pair =
|
||||||
if WildcardDomainChecker.check_domain(domain, return_url_host)
|
provider_secrets.find do |domain, configured_secret|
|
||||||
first_domain_match ||= configured_secret
|
if WildcardDomainChecker.check_domain(domain, return_url_host)
|
||||||
sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"]
|
first_domain_match ||= configured_secret
|
||||||
|
sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# falls back to a secret which will fail to validate in DiscourseConnectBase
|
# falls back to a secret which will fail to validate in DiscourseConnectBase
|
||||||
# this ensures error flow is correct
|
# this ensures error flow is correct
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'rails'
|
require "rails"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Category < Record
|
class Category < Record
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super(::Category, DiscourseDev.config.category[:count])
|
super(::Category, DiscourseDev.config.category[:count])
|
||||||
@parent_category_ids = ::Category.where(parent_category_id: nil).pluck(:id)
|
@parent_category_ids = ::Category.where(parent_category_id: nil).pluck(:id)
|
||||||
@ -29,7 +28,7 @@ module DiscourseDev
|
|||||||
description: Faker::Lorem.paragraph,
|
description: Faker::Lorem.paragraph,
|
||||||
user_id: ::Discourse::SYSTEM_USER_ID,
|
user_id: ::Discourse::SYSTEM_USER_ID,
|
||||||
color: Faker::Color.hex_color.last(6),
|
color: Faker::Color.hex_color.last(6),
|
||||||
parent_category_id: parent_category_id
|
parent_category_id: parent_category_id,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rails'
|
require "rails"
|
||||||
require 'highline/import'
|
require "highline/import"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Config
|
class Config
|
||||||
@ -63,10 +63,11 @@ module DiscourseDev
|
|||||||
if settings.present?
|
if settings.present?
|
||||||
email = settings[:email] || "new_user@example.com"
|
email = settings[:email] || "new_user@example.com"
|
||||||
|
|
||||||
new_user = ::User.create!(
|
new_user =
|
||||||
email: email,
|
::User.create!(
|
||||||
username: settings[:username] || UserNameSuggester.suggest(email)
|
email: email,
|
||||||
)
|
username: settings[:username] || UserNameSuggester.suggest(email),
|
||||||
|
)
|
||||||
new_user.email_tokens.update_all confirmed: true
|
new_user.email_tokens.update_all confirmed: true
|
||||||
new_user.activate
|
new_user.activate
|
||||||
end
|
end
|
||||||
@ -88,15 +89,14 @@ module DiscourseDev
|
|||||||
def create_admin_user_from_settings(settings)
|
def create_admin_user_from_settings(settings)
|
||||||
email = settings[:email]
|
email = settings[:email]
|
||||||
|
|
||||||
admin = ::User.with_email(email).first_or_create!(
|
admin =
|
||||||
email: email,
|
::User.with_email(email).first_or_create!(
|
||||||
username: settings[:username] || UserNameSuggester.suggest(email),
|
email: email,
|
||||||
password: settings[:password]
|
username: settings[:username] || UserNameSuggester.suggest(email),
|
||||||
)
|
password: settings[:password],
|
||||||
|
)
|
||||||
admin.grant_admin!
|
admin.grant_admin!
|
||||||
if admin.trust_level < 1
|
admin.change_trust_level!(1) if admin.trust_level < 1
|
||||||
admin.change_trust_level!(1)
|
|
||||||
end
|
|
||||||
admin.email_tokens.update_all confirmed: true
|
admin.email_tokens.update_all confirmed: true
|
||||||
admin.activate
|
admin.activate
|
||||||
end
|
end
|
||||||
@ -107,10 +107,7 @@ module DiscourseDev
|
|||||||
password = ask("Password (optional, press ENTER to skip): ")
|
password = ask("Password (optional, press ENTER to skip): ")
|
||||||
username = UserNameSuggester.suggest(email)
|
username = UserNameSuggester.suggest(email)
|
||||||
|
|
||||||
admin = ::User.new(
|
admin = ::User.new(email: email, username: username)
|
||||||
email: email,
|
|
||||||
username: username
|
|
||||||
)
|
|
||||||
|
|
||||||
if password.present?
|
if password.present?
|
||||||
admin.password = password
|
admin.password = password
|
||||||
@ -122,7 +119,7 @@ module DiscourseDev
|
|||||||
saved = admin.save
|
saved = admin.save
|
||||||
|
|
||||||
if saved
|
if saved
|
||||||
File.open(file_path, 'a') do | file|
|
File.open(file_path, "a") do |file|
|
||||||
file.puts("admin:")
|
file.puts("admin:")
|
||||||
file.puts(" username: #{admin.username}")
|
file.puts(" username: #{admin.username}")
|
||||||
file.puts(" email: #{admin.email}")
|
file.puts(" email: #{admin.email}")
|
||||||
@ -137,9 +134,7 @@ module DiscourseDev
|
|||||||
admin.save
|
admin.save
|
||||||
|
|
||||||
admin.grant_admin!
|
admin.grant_admin!
|
||||||
if admin.trust_level < 1
|
admin.change_trust_level!(1) if admin.trust_level < 1
|
||||||
admin.change_trust_level!(1)
|
|
||||||
end
|
|
||||||
admin.email_tokens.update_all confirmed: true
|
admin.email_tokens.update_all confirmed: true
|
||||||
admin.activate
|
admin.activate
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'rails'
|
require "rails"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Group < Record
|
class Group < Record
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super(::Group, DiscourseDev.config.group[:count])
|
super(::Group, DiscourseDev.config.group[:count])
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Post < Record
|
class Post < Record
|
||||||
|
|
||||||
attr_reader :topic
|
attr_reader :topic
|
||||||
|
|
||||||
def initialize(topic, count)
|
def initialize(topic, count)
|
||||||
@ -28,7 +27,7 @@ module DiscourseDev
|
|||||||
raw: Faker::DiscourseMarkdown.sandwich(sentences: 5),
|
raw: Faker::DiscourseMarkdown.sandwich(sentences: 5),
|
||||||
created_at: Faker::Time.between(from: topic.last_posted_at, to: DateTime.now),
|
created_at: Faker::Time.between(from: topic.last_posted_at, to: DateTime.now),
|
||||||
skip_validations: true,
|
skip_validations: true,
|
||||||
skip_guardian: true
|
skip_guardian: true,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -44,13 +43,20 @@ module DiscourseDev
|
|||||||
def generate_likes(post)
|
def generate_likes(post)
|
||||||
user_ids = [post.user_id]
|
user_ids = [post.user_id]
|
||||||
|
|
||||||
Faker::Number.between(from: 0, to: @max_likes_count).times do
|
Faker::Number
|
||||||
user = self.user
|
.between(from: 0, to: @max_likes_count)
|
||||||
next if user_ids.include?(user.id)
|
.times do
|
||||||
|
user = self.user
|
||||||
|
next if user_ids.include?(user.id)
|
||||||
|
|
||||||
PostActionCreator.new(user, post, PostActionType.types[:like], created_at: Faker::Time.between(from: post.created_at, to: DateTime.now)).perform
|
PostActionCreator.new(
|
||||||
user_ids << user.id
|
user,
|
||||||
end
|
post,
|
||||||
|
PostActionType.types[:like],
|
||||||
|
created_at: Faker::Time.between(from: post.created_at, to: DateTime.now),
|
||||||
|
).perform
|
||||||
|
user_ids << user.id
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def user
|
def user
|
||||||
@ -90,13 +96,14 @@ module DiscourseDev
|
|||||||
count.times do |i|
|
count.times do |i|
|
||||||
begin
|
begin
|
||||||
user = User.random
|
user = User.random
|
||||||
reply = Faker::DiscourseMarkdown.with_user(user.id) do
|
reply =
|
||||||
{
|
Faker::DiscourseMarkdown.with_user(user.id) do
|
||||||
topic_id: topic.id,
|
{
|
||||||
raw: Faker::DiscourseMarkdown.sandwich(sentences: 5),
|
topic_id: topic.id,
|
||||||
skip_validations: true
|
raw: Faker::DiscourseMarkdown.sandwich(sentences: 5),
|
||||||
}
|
skip_validations: true,
|
||||||
end
|
}
|
||||||
|
end
|
||||||
PostCreator.new(user, reply).create!
|
PostCreator.new(user, reply).create!
|
||||||
rescue ActiveRecord::RecordNotSaved => e
|
rescue ActiveRecord::RecordNotSaved => e
|
||||||
puts e
|
puts e
|
||||||
@ -109,6 +116,5 @@ module DiscourseDev
|
|||||||
def self.random
|
def self.random
|
||||||
super(::Post)
|
super(::Post)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class PostRevision < Record
|
class PostRevision < Record
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super(::PostRevision, DiscourseDev.config.post_revisions[:count])
|
super(::PostRevision, DiscourseDev.config.post_revisions[:count])
|
||||||
end
|
end
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev'
|
require "discourse_dev"
|
||||||
require 'rails'
|
require "rails"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Record
|
class Record
|
||||||
@ -12,11 +12,12 @@ module DiscourseDev
|
|||||||
attr_reader :model, :type
|
attr_reader :model, :type
|
||||||
|
|
||||||
def initialize(model, count = DEFAULT_COUNT)
|
def initialize(model, count = DEFAULT_COUNT)
|
||||||
@@initialized ||= begin
|
@@initialized ||=
|
||||||
Faker::Discourse.unique.clear
|
begin
|
||||||
RateLimiter.disable
|
Faker::Discourse.unique.clear
|
||||||
true
|
RateLimiter.disable
|
||||||
end
|
true
|
||||||
|
end
|
||||||
|
|
||||||
@model = model
|
@model = model
|
||||||
@type = model.to_s.downcase.to_sym
|
@type = model.to_s.downcase.to_sym
|
||||||
@ -40,11 +41,9 @@ module DiscourseDev
|
|||||||
if current_count >= @count
|
if current_count >= @count
|
||||||
puts "Already have #{current_count} #{type} records"
|
puts "Already have #{current_count} #{type} records"
|
||||||
|
|
||||||
Rake.application.top_level_tasks.each do |task_name|
|
Rake.application.top_level_tasks.each { |task_name| Rake::Task[task_name].reenable }
|
||||||
Rake::Task[task_name].reenable
|
|
||||||
end
|
|
||||||
|
|
||||||
Rake::Task['dev:repopulate'].invoke
|
Rake::Task["dev:repopulate"].invoke
|
||||||
return
|
return
|
||||||
elsif current_count > 0
|
elsif current_count > 0
|
||||||
@count -= current_count
|
@count -= current_count
|
||||||
@ -74,7 +73,9 @@ module DiscourseDev
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.random(model, use_existing_records: true)
|
def self.random(model, use_existing_records: true)
|
||||||
model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") if !use_existing_records && model.new.respond_to?(:custom_fields)
|
if !use_existing_records && model.new.respond_to?(:custom_fields)
|
||||||
|
model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'")
|
||||||
|
end
|
||||||
count = model.count
|
count = model.count
|
||||||
raise "#{:type} records are not yet populated" if count == 0
|
raise "#{:type} records are not yet populated" if count == 0
|
||||||
|
|
||||||
|
@ -1,12 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'rails'
|
require "rails"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Tag < Record
|
class Tag < Record
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super(::Tag, DiscourseDev.config.tag[:count])
|
super(::Tag, DiscourseDev.config.tag[:count])
|
||||||
end
|
end
|
||||||
@ -24,9 +23,7 @@ module DiscourseDev
|
|||||||
end
|
end
|
||||||
|
|
||||||
def data
|
def data
|
||||||
{
|
{ name: Faker::Discourse.unique.tag }
|
||||||
name: Faker::Discourse.unique.tag,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'discourse_dev/record'
|
require "discourse_dev/record"
|
||||||
require 'faker'
|
require "faker"
|
||||||
|
|
||||||
module DiscourseDev
|
module DiscourseDev
|
||||||
class Topic < Record
|
class Topic < Record
|
||||||
|
|
||||||
def initialize(private_messages: false, recipient: nil, ignore_current_count: false)
|
def initialize(private_messages: false, recipient: nil, ignore_current_count: false)
|
||||||
@settings = DiscourseDev.config.topic
|
@settings = DiscourseDev.config.topic
|
||||||
@private_messages = private_messages
|
@private_messages = private_messages
|
||||||
@ -33,15 +32,9 @@ module DiscourseDev
|
|||||||
end
|
end
|
||||||
|
|
||||||
if @category
|
if @category
|
||||||
merge_attributes = {
|
merge_attributes = { category: @category.id, tags: tags }
|
||||||
category: @category.id,
|
|
||||||
tags: tags
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
merge_attributes = {
|
merge_attributes = { archetype: "private_message", target_usernames: [@recipient] }
|
||||||
archetype: "private_message",
|
|
||||||
target_usernames: [@recipient]
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -51,9 +44,11 @@ module DiscourseDev
|
|||||||
topic_opts: {
|
topic_opts: {
|
||||||
import_mode: true,
|
import_mode: true,
|
||||||
views: Faker::Number.between(from: 1, to: max_views),
|
views: Faker::Number.between(from: 1, to: max_views),
|
||||||
custom_fields: { dev_sample: true }
|
custom_fields: {
|
||||||
|
dev_sample: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
skip_validations: true
|
skip_validations: true,
|
||||||
}.merge(merge_attributes)
|
}.merge(merge_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -61,7 +56,10 @@ module DiscourseDev
|
|||||||
if current_count < I18n.t("faker.discourse.topics").count
|
if current_count < I18n.t("faker.discourse.topics").count
|
||||||
Faker::Discourse.unique.topic
|
Faker::Discourse.unique.topic
|
||||||
else
|
else
|
||||||
Faker::Lorem.unique.sentence(word_count: 5, supplemental: true, random_words_to_add: 4).chomp(".")
|
Faker::Lorem
|
||||||
|
.unique
|
||||||
|
.sentence(word_count: 5, supplemental: true, random_words_to_add: 4)
|
||||||
|
.chomp(".")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -70,9 +68,9 @@ module DiscourseDev
|
|||||||
|
|
||||||
@tags = []
|
@tags = []
|
||||||
|
|
||||||
Faker::Number.between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)).times do
|
Faker::Number
|
||||||
@tags << Faker::Discourse.tag
|
.between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max))
|
||||||
end
|
.times { @tags << Faker::Discourse.tag }
|
||||||
|
|
||||||
@tags.uniq
|
@tags.uniq
|
||||||
end
|
end
|
||||||
@ -92,7 +90,11 @@ module DiscourseDev
|
|||||||
if override = @settings.dig(:replies, :overrides).find { |o| o[:title] == topic_data[:title] }
|
if override = @settings.dig(:replies, :overrides).find { |o| o[:title] == topic_data[:title] }
|
||||||
reply_count = override[:count]
|
reply_count = override[:count]
|
||||||
else
|
else
|
||||||
reply_count = Faker::Number.between(from: @settings.dig(:replies, :min), to: @settings.dig(:replies, :max))
|
reply_count =
|
||||||
|
Faker::Number.between(
|
||||||
|
from: @settings.dig(:replies, :min),
|
||||||
|
to: @settings.dig(:replies, :max),
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
topic = post.topic
|
topic = post.topic
|
||||||
@ -123,9 +125,7 @@ module DiscourseDev
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_unwanted_sidekiq_jobs
|
def delete_unwanted_sidekiq_jobs
|
||||||
Sidekiq::ScheduledSet.new.each do |job|
|
Sidekiq::ScheduledSet.new.each { |job| job.delete if job.item["class"] == "Jobs::UserEmail" }
|
||||||
job.delete if job.item["class"] == "Jobs::UserEmail"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class DiscourseDiff
|
class DiscourseDiff
|
||||||
|
|
||||||
MAX_DIFFERENCE = 200
|
MAX_DIFFERENCE = 200
|
||||||
|
|
||||||
def initialize(before, after)
|
def initialize(before, after)
|
||||||
@ -9,8 +8,8 @@ class DiscourseDiff
|
|||||||
@after = after
|
@after = after
|
||||||
before_html = tokenize_html_blocks(@before)
|
before_html = tokenize_html_blocks(@before)
|
||||||
after_html = tokenize_html_blocks(@after)
|
after_html = tokenize_html_blocks(@after)
|
||||||
before_markdown = tokenize_line(CGI::escapeHTML(@before))
|
before_markdown = tokenize_line(CGI.escapeHTML(@before))
|
||||||
after_markdown = tokenize_line(CGI::escapeHTML(@after))
|
after_markdown = tokenize_line(CGI.escapeHTML(@after))
|
||||||
|
|
||||||
@block_by_block_diff = ONPDiff.new(before_html, after_html).paragraph_diff
|
@block_by_block_diff = ONPDiff.new(before_html, after_html).paragraph_diff
|
||||||
@line_by_line_diff = ONPDiff.new(before_markdown, after_markdown).short_diff
|
@line_by_line_diff = ONPDiff.new(before_markdown, after_markdown).short_diff
|
||||||
@ -21,7 +20,8 @@ class DiscourseDiff
|
|||||||
inline = []
|
inline = []
|
||||||
while i < @block_by_block_diff.size
|
while i < @block_by_block_diff.size
|
||||||
op_code = @block_by_block_diff[i][1]
|
op_code = @block_by_block_diff[i][1]
|
||||||
if op_code == :common then inline << @block_by_block_diff[i][0]
|
if op_code == :common
|
||||||
|
inline << @block_by_block_diff[i][0]
|
||||||
else
|
else
|
||||||
if op_code == :delete
|
if op_code == :delete
|
||||||
opposite_op_code = :add
|
opposite_op_code = :add
|
||||||
@ -36,7 +36,11 @@ class DiscourseDiff
|
|||||||
end
|
end
|
||||||
|
|
||||||
if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code
|
if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code
|
||||||
diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff
|
diff =
|
||||||
|
ONPDiff.new(
|
||||||
|
tokenize_html(@block_by_block_diff[first][0]),
|
||||||
|
tokenize_html(@block_by_block_diff[second][0]),
|
||||||
|
).diff
|
||||||
inline << generate_inline_html(diff)
|
inline << generate_inline_html(diff)
|
||||||
i += 1
|
i += 1
|
||||||
else
|
else
|
||||||
@ -73,7 +77,11 @@ class DiscourseDiff
|
|||||||
end
|
end
|
||||||
|
|
||||||
if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code
|
if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code
|
||||||
diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff
|
diff =
|
||||||
|
ONPDiff.new(
|
||||||
|
tokenize_html(@block_by_block_diff[first][0]),
|
||||||
|
tokenize_html(@block_by_block_diff[second][0]),
|
||||||
|
).diff
|
||||||
deleted, inserted = generate_side_by_side_html(diff)
|
deleted, inserted = generate_side_by_side_html(diff)
|
||||||
left << deleted
|
left << deleted
|
||||||
right << inserted
|
right << inserted
|
||||||
@ -109,9 +117,13 @@ class DiscourseDiff
|
|||||||
end
|
end
|
||||||
|
|
||||||
if i + 1 < @line_by_line_diff.size && @line_by_line_diff[i + 1][1] == opposite_op_code
|
if i + 1 < @line_by_line_diff.size && @line_by_line_diff[i + 1][1] == opposite_op_code
|
||||||
before_tokens, after_tokens = tokenize_markdown(@line_by_line_diff[first][0]), tokenize_markdown(@line_by_line_diff[second][0])
|
before_tokens, after_tokens =
|
||||||
|
tokenize_markdown(@line_by_line_diff[first][0]),
|
||||||
|
tokenize_markdown(@line_by_line_diff[second][0])
|
||||||
if (before_tokens.size - after_tokens.size).abs > MAX_DIFFERENCE
|
if (before_tokens.size - after_tokens.size).abs > MAX_DIFFERENCE
|
||||||
before_tokens, after_tokens = tokenize_line(@line_by_line_diff[first][0]), tokenize_line(@line_by_line_diff[second][0])
|
before_tokens, after_tokens =
|
||||||
|
tokenize_line(@line_by_line_diff[first][0]),
|
||||||
|
tokenize_line(@line_by_line_diff[second][0])
|
||||||
end
|
end
|
||||||
diff = ONPDiff.new(before_tokens, after_tokens).short_diff
|
diff = ONPDiff.new(before_tokens, after_tokens).short_diff
|
||||||
deleted, inserted = generate_side_by_side_markdown(diff)
|
deleted, inserted = generate_side_by_side_markdown(diff)
|
||||||
@ -178,7 +190,7 @@ class DiscourseDiff
|
|||||||
def add_class_or_wrap_in_tags(html_or_text, klass)
|
def add_class_or_wrap_in_tags(html_or_text, klass)
|
||||||
result = html_or_text.dup
|
result = html_or_text.dup
|
||||||
index_of_next_chevron = result.index(">")
|
index_of_next_chevron = result.index(">")
|
||||||
if result.size > 0 && result[0] == '<' && index_of_next_chevron
|
if result.size > 0 && result[0] == "<" && index_of_next_chevron
|
||||||
index_of_class = result.index("class=")
|
index_of_class = result.index("class=")
|
||||||
if index_of_class.nil? || index_of_class > index_of_next_chevron
|
if index_of_class.nil? || index_of_class > index_of_next_chevron
|
||||||
# we do not have a class for the current tag
|
# we do not have a class for the current tag
|
||||||
@ -202,9 +214,12 @@ class DiscourseDiff
|
|||||||
inline = []
|
inline = []
|
||||||
diff.each do |d|
|
diff.each do |d|
|
||||||
case d[1]
|
case d[1]
|
||||||
when :common then inline << d[0]
|
when :common
|
||||||
when :delete then inline << add_class_or_wrap_in_tags(d[0], "del")
|
inline << d[0]
|
||||||
when :add then inline << add_class_or_wrap_in_tags(d[0], "ins")
|
when :delete
|
||||||
|
inline << add_class_or_wrap_in_tags(d[0], "del")
|
||||||
|
when :add
|
||||||
|
inline << add_class_or_wrap_in_tags(d[0], "ins")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
inline
|
inline
|
||||||
@ -217,8 +232,10 @@ class DiscourseDiff
|
|||||||
when :common
|
when :common
|
||||||
deleted << d[0]
|
deleted << d[0]
|
||||||
inserted << d[0]
|
inserted << d[0]
|
||||||
when :delete then deleted << add_class_or_wrap_in_tags(d[0], "del")
|
when :delete
|
||||||
when :add then inserted << add_class_or_wrap_in_tags(d[0], "ins")
|
deleted << add_class_or_wrap_in_tags(d[0], "del")
|
||||||
|
when :add
|
||||||
|
inserted << add_class_or_wrap_in_tags(d[0], "ins")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
[deleted, inserted]
|
[deleted, inserted]
|
||||||
@ -231,15 +248,16 @@ class DiscourseDiff
|
|||||||
when :common
|
when :common
|
||||||
deleted << d[0]
|
deleted << d[0]
|
||||||
inserted << d[0]
|
inserted << d[0]
|
||||||
when :delete then deleted << "<del>#{d[0]}</del>"
|
when :delete
|
||||||
when :add then inserted << "<ins>#{d[0]}</ins>"
|
deleted << "<del>#{d[0]}</del>"
|
||||||
|
when :add
|
||||||
|
inserted << "<ins>#{d[0]}</ins>"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
[deleted, inserted]
|
[deleted, inserted]
|
||||||
end
|
end
|
||||||
|
|
||||||
class HtmlTokenizer < Nokogiri::XML::SAX::Document
|
class HtmlTokenizer < Nokogiri::XML::SAX::Document
|
||||||
|
|
||||||
attr_accessor :tokens
|
attr_accessor :tokens
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@ -253,23 +271,21 @@ class DiscourseDiff
|
|||||||
me.tokens
|
me.tokens
|
||||||
end
|
end
|
||||||
|
|
||||||
USELESS_TAGS = %w{html body}
|
USELESS_TAGS = %w[html body]
|
||||||
def start_element(name, attributes = [])
|
def start_element(name, attributes = [])
|
||||||
return if USELESS_TAGS.include?(name)
|
return if USELESS_TAGS.include?(name)
|
||||||
attrs = attributes.map { |a| " #{a[0]}=\"#{CGI::escapeHTML(a[1])}\"" }.join
|
attrs = attributes.map { |a| " #{a[0]}=\"#{CGI.escapeHTML(a[1])}\"" }.join
|
||||||
@tokens << "<#{name}#{attrs}>"
|
@tokens << "<#{name}#{attrs}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
AUTOCLOSING_TAGS = %w{area base br col embed hr img input meta}
|
AUTOCLOSING_TAGS = %w[area base br col embed hr img input meta]
|
||||||
def end_element(name)
|
def end_element(name)
|
||||||
return if USELESS_TAGS.include?(name) || AUTOCLOSING_TAGS.include?(name)
|
return if USELESS_TAGS.include?(name) || AUTOCLOSING_TAGS.include?(name)
|
||||||
@tokens << "</#{name}>"
|
@tokens << "</#{name}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def characters(string)
|
def characters(string)
|
||||||
@tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI::escapeHTML(x) }
|
@tokens.concat string.scan(/\W|\w+[ \t]*/).map { |x| CGI.escapeHTML(x) }
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -3,21 +3,23 @@
|
|||||||
# This is meant to be used by plugins to trigger and listen to events
|
# This is meant to be used by plugins to trigger and listen to events
|
||||||
# So we can execute code when things happen.
|
# So we can execute code when things happen.
|
||||||
class DiscourseEvent
|
class DiscourseEvent
|
||||||
|
|
||||||
# Defaults to a hash where default values are empty sets.
|
# Defaults to a hash where default values are empty sets.
|
||||||
def self.events
|
def self.events
|
||||||
@events ||= Hash.new { |hash, key| hash[key] = Set.new }
|
@events ||= Hash.new { |hash, key| hash[key] = Set.new }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.trigger(event_name, *args, **kwargs)
|
def self.trigger(event_name, *args, **kwargs)
|
||||||
events[event_name].each do |event|
|
events[event_name].each { |event| event.call(*args, **kwargs) }
|
||||||
event.call(*args, **kwargs)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.on(event_name, &block)
|
def self.on(event_name, &block)
|
||||||
if event_name == :site_setting_saved
|
if event_name == :site_setting_saved
|
||||||
Discourse.deprecate("The :site_setting_saved event is deprecated. Please use :site_setting_changed instead", since: "2.3.0beta8", drop_from: "2.4", raise_error: true)
|
Discourse.deprecate(
|
||||||
|
"The :site_setting_saved event is deprecated. Please use :site_setting_changed instead",
|
||||||
|
since: "2.3.0beta8",
|
||||||
|
drop_from: "2.4",
|
||||||
|
raise_error: true,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
events[event_name] << block
|
events[event_name] << block
|
||||||
end
|
end
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DiscourseHub
|
module DiscourseHub
|
||||||
|
|
||||||
STATS_FETCHED_AT_KEY = "stats_fetched_at"
|
STATS_FETCHED_AT_KEY = "stats_fetched_at"
|
||||||
|
|
||||||
def self.version_check_payload
|
def self.version_check_payload
|
||||||
default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch })
|
default_payload = { installed_version: Discourse::VERSION::STRING }.merge!(
|
||||||
|
Discourse.git_branch == "unknown" ? {} : { branch: Discourse.git_branch },
|
||||||
|
)
|
||||||
default_payload.merge!(get_payload)
|
default_payload.merge!(get_payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.discourse_version_check
|
def self.discourse_version_check
|
||||||
get('/version_check', version_check_payload)
|
get("/version_check", version_check_payload)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.stats_fetched_at=(time_with_zone)
|
def self.stats_fetched_at=(time_with_zone)
|
||||||
@ -18,7 +19,11 @@ module DiscourseHub
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.get_payload
|
def self.get_payload
|
||||||
SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago ? About.fetch_cached_stats.symbolize_keys : {}
|
if SiteSetting.share_anonymized_statistics && stats_fetched_at < 7.days.ago
|
||||||
|
About.fetch_cached_stats.symbolize_keys
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.get(rel_url, params = {})
|
def self.get(rel_url, params = {})
|
||||||
@ -40,27 +45,39 @@ module DiscourseHub
|
|||||||
def self.singular_action(action, rel_url, params = {})
|
def self.singular_action(action, rel_url, params = {})
|
||||||
connect_opts = connect_opts(params)
|
connect_opts = connect_opts(params)
|
||||||
|
|
||||||
JSON.parse(Excon.public_send(action,
|
JSON.parse(
|
||||||
"#{hub_base_url}#{rel_url}",
|
Excon.public_send(
|
||||||
{
|
action,
|
||||||
headers: { 'Referer' => referer, 'Accept' => accepts.join(', ') },
|
"#{hub_base_url}#{rel_url}",
|
||||||
query: params,
|
{
|
||||||
omit_default_port: true
|
headers: {
|
||||||
}.merge(connect_opts)
|
"Referer" => referer,
|
||||||
).body)
|
"Accept" => accepts.join(", "),
|
||||||
|
},
|
||||||
|
query: params,
|
||||||
|
omit_default_port: true,
|
||||||
|
}.merge(connect_opts),
|
||||||
|
).body,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.collection_action(action, rel_url, params = {})
|
def self.collection_action(action, rel_url, params = {})
|
||||||
connect_opts = connect_opts(params)
|
connect_opts = connect_opts(params)
|
||||||
|
|
||||||
response = Excon.public_send(action,
|
response =
|
||||||
"#{hub_base_url}#{rel_url}",
|
Excon.public_send(
|
||||||
{
|
action,
|
||||||
body: JSON[params],
|
"#{hub_base_url}#{rel_url}",
|
||||||
headers: { 'Referer' => referer, 'Accept' => accepts.join(', '), "Content-Type" => "application/json" },
|
{
|
||||||
omit_default_port: true
|
body: JSON[params],
|
||||||
}.merge(connect_opts)
|
headers: {
|
||||||
)
|
"Referer" => referer,
|
||||||
|
"Accept" => accepts.join(", "),
|
||||||
|
"Content-Type" => "application/json",
|
||||||
|
},
|
||||||
|
omit_default_port: true,
|
||||||
|
}.merge(connect_opts),
|
||||||
|
)
|
||||||
|
|
||||||
if (status = response.status) != 200
|
if (status = response.status) != 200
|
||||||
Rails.logger.warn(response_status_log_message(rel_url, status))
|
Rails.logger.warn(response_status_log_message(rel_url, status))
|
||||||
@ -87,14 +104,14 @@ module DiscourseHub
|
|||||||
|
|
||||||
def self.hub_base_url
|
def self.hub_base_url
|
||||||
if Rails.env.production?
|
if Rails.env.production?
|
||||||
ENV['HUB_BASE_URL'] || 'https://api.discourse.org/api'
|
ENV["HUB_BASE_URL"] || "https://api.discourse.org/api"
|
||||||
else
|
else
|
||||||
ENV['HUB_BASE_URL'] || 'http://local.hub:3000/api'
|
ENV["HUB_BASE_URL"] || "http://local.hub:3000/api"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.accepts
|
def self.accepts
|
||||||
['application/json', 'application/vnd.discoursehub.v1']
|
%w[application/json application/vnd.discoursehub.v1]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.referer
|
def self.referer
|
||||||
@ -105,5 +122,4 @@ module DiscourseHub
|
|||||||
t = Discourse.redis.get(STATS_FETCHED_AT_KEY)
|
t = Discourse.redis.get(STATS_FETCHED_AT_KEY)
|
||||||
t ? Time.zone.at(t.to_i) : 1.year.ago
|
t ? Time.zone.at(t.to_i) : 1.year.ago
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'maxminddb'
|
require "maxminddb"
|
||||||
require 'resolv'
|
require "resolv"
|
||||||
|
|
||||||
class DiscourseIpInfo
|
class DiscourseIpInfo
|
||||||
include Singleton
|
include Singleton
|
||||||
@ -11,13 +11,13 @@ class DiscourseIpInfo
|
|||||||
end
|
end
|
||||||
|
|
||||||
def open_db(path)
|
def open_db(path)
|
||||||
@loc_mmdb = mmdb_load(File.join(path, 'GeoLite2-City.mmdb'))
|
@loc_mmdb = mmdb_load(File.join(path, "GeoLite2-City.mmdb"))
|
||||||
@asn_mmdb = mmdb_load(File.join(path, 'GeoLite2-ASN.mmdb'))
|
@asn_mmdb = mmdb_load(File.join(path, "GeoLite2-ASN.mmdb"))
|
||||||
@cache = LruRedux::ThreadSafeCache.new(2000)
|
@cache = LruRedux::ThreadSafeCache.new(2000)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.path
|
def self.path
|
||||||
@path ||= File.join(Rails.root, 'vendor', 'data')
|
@path ||= File.join(Rails.root, "vendor", "data")
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.mmdb_path(name)
|
def self.mmdb_path(name)
|
||||||
@ -25,7 +25,6 @@ class DiscourseIpInfo
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.mmdb_download(name)
|
def self.mmdb_download(name)
|
||||||
|
|
||||||
if GlobalSetting.maxmind_license_key.blank?
|
if GlobalSetting.maxmind_license_key.blank?
|
||||||
STDERR.puts "MaxMind IP database updates require a license"
|
STDERR.puts "MaxMind IP database updates require a license"
|
||||||
STDERR.puts "Please set DISCOURSE_MAXMIND_LICENSE_KEY to one you generated at https://www.maxmind.com"
|
STDERR.puts "Please set DISCOURSE_MAXMIND_LICENSE_KEY to one you generated at https://www.maxmind.com"
|
||||||
@ -34,41 +33,29 @@ class DiscourseIpInfo
|
|||||||
|
|
||||||
FileUtils.mkdir_p(path)
|
FileUtils.mkdir_p(path)
|
||||||
|
|
||||||
url = "https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz"
|
url =
|
||||||
|
"https://download.maxmind.com/app/geoip_download?license_key=#{GlobalSetting.maxmind_license_key}&edition_id=#{name}&suffix=tar.gz"
|
||||||
|
|
||||||
gz_file = FileHelper.download(
|
gz_file =
|
||||||
url,
|
FileHelper.download(
|
||||||
max_file_size: 100.megabytes,
|
url,
|
||||||
tmp_file_name: "#{name}.gz",
|
max_file_size: 100.megabytes,
|
||||||
validate_uri: false,
|
tmp_file_name: "#{name}.gz",
|
||||||
follow_redirect: false
|
validate_uri: false,
|
||||||
)
|
follow_redirect: false,
|
||||||
|
)
|
||||||
|
|
||||||
filename = File.basename(gz_file.path)
|
filename = File.basename(gz_file.path)
|
||||||
|
|
||||||
dir = "#{Dir.tmpdir}/#{SecureRandom.hex}"
|
dir = "#{Dir.tmpdir}/#{SecureRandom.hex}"
|
||||||
|
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command("mkdir", "-p", dir)
|
||||||
"mkdir", "-p", dir
|
|
||||||
)
|
|
||||||
|
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command("cp", gz_file.path, "#{dir}/#{filename}")
|
||||||
"cp",
|
|
||||||
gz_file.path,
|
|
||||||
"#{dir}/#{filename}"
|
|
||||||
)
|
|
||||||
|
|
||||||
Discourse::Utils.execute_command(
|
Discourse::Utils.execute_command("tar", "-xzvf", "#{dir}/#{filename}", chdir: dir)
|
||||||
"tar",
|
|
||||||
"-xzvf",
|
|
||||||
"#{dir}/#{filename}",
|
|
||||||
chdir: dir
|
|
||||||
)
|
|
||||||
|
|
||||||
Dir["#{dir}/**/*.mmdb"].each do |f|
|
|
||||||
FileUtils.mv(f, mmdb_path(name))
|
|
||||||
end
|
|
||||||
|
|
||||||
|
Dir["#{dir}/**/*.mmdb"].each { |f| FileUtils.mv(f, mmdb_path(name)) }
|
||||||
ensure
|
ensure
|
||||||
FileUtils.rm_r(dir, force: true) if dir
|
FileUtils.rm_r(dir, force: true) if dir
|
||||||
gz_file&.close!
|
gz_file&.close!
|
||||||
@ -96,7 +83,8 @@ class DiscourseIpInfo
|
|||||||
if result&.found?
|
if result&.found?
|
||||||
ret[:country] = result.country.name(locale) || result.country.name
|
ret[:country] = result.country.name(locale) || result.country.name
|
||||||
ret[:country_code] = result.country.iso_code
|
ret[:country_code] = result.country.iso_code
|
||||||
ret[:region] = result.subdivisions.most_specific.name(locale) || result.subdivisions.most_specific.name
|
ret[:region] = result.subdivisions.most_specific.name(locale) ||
|
||||||
|
result.subdivisions.most_specific.name
|
||||||
ret[:city] = result.city.name(locale) || result.city.name
|
ret[:city] = result.city.name(locale) || result.city.name
|
||||||
ret[:latitude] = result.location.latitude
|
ret[:latitude] = result.location.latitude
|
||||||
ret[:longitude] = result.location.longitude
|
ret[:longitude] = result.location.longitude
|
||||||
@ -104,13 +92,18 @@ class DiscourseIpInfo
|
|||||||
|
|
||||||
# used by plugins or API to locate users more accurately
|
# used by plugins or API to locate users more accurately
|
||||||
ret[:geoname_ids] = [
|
ret[:geoname_ids] = [
|
||||||
result.continent.geoname_id, result.country.geoname_id, result.city.geoname_id,
|
result.continent.geoname_id,
|
||||||
*result.subdivisions.map(&:geoname_id)
|
result.country.geoname_id,
|
||||||
|
result.city.geoname_id,
|
||||||
|
*result.subdivisions.map(&:geoname_id),
|
||||||
]
|
]
|
||||||
ret[:geoname_ids].compact!
|
ret[:geoname_ids].compact!
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.")
|
Discourse.warn_exception(
|
||||||
|
e,
|
||||||
|
message: "IP #{ip} could not be looked up in MaxMind GeoLite2-City database.",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -123,7 +116,10 @@ class DiscourseIpInfo
|
|||||||
ret[:organization] = result["autonomous_system_organization"]
|
ret[:organization] = result["autonomous_system_organization"]
|
||||||
end
|
end
|
||||||
rescue => e
|
rescue => e
|
||||||
Discourse.warn_exception(e, message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.")
|
Discourse.warn_exception(
|
||||||
|
e,
|
||||||
|
message: "IP #{ip} could not be looked up in MaxMind GeoLite2-ASN database.",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -142,10 +138,13 @@ class DiscourseIpInfo
|
|||||||
|
|
||||||
def get(ip, locale: :en, resolve_hostname: false)
|
def get(ip, locale: :en, resolve_hostname: false)
|
||||||
ip = ip.to_s
|
ip = ip.to_s
|
||||||
locale = locale.to_s.sub('_', '-')
|
locale = locale.to_s.sub("_", "-")
|
||||||
|
|
||||||
@cache["#{ip}-#{locale}-#{resolve_hostname}"] ||=
|
@cache["#{ip}-#{locale}-#{resolve_hostname}"] ||= lookup(
|
||||||
lookup(ip, locale: locale, resolve_hostname: resolve_hostname)
|
ip,
|
||||||
|
locale: locale,
|
||||||
|
resolve_hostname: resolve_hostname,
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.open_db(path)
|
def self.open_db(path)
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
require 'execjs'
|
require "execjs"
|
||||||
require 'mini_racer'
|
require "mini_racer"
|
||||||
|
|
||||||
class DiscourseJsProcessor
|
class DiscourseJsProcessor
|
||||||
class TranspileError < StandardError; end
|
class TranspileError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
DISCOURSE_COMMON_BABEL_PLUGINS = [
|
DISCOURSE_COMMON_BABEL_PLUGINS = [
|
||||||
'proposal-optional-chaining',
|
"proposal-optional-chaining",
|
||||||
['proposal-decorators', { legacy: true } ],
|
["proposal-decorators", { legacy: true }],
|
||||||
'transform-template-literals',
|
"transform-template-literals",
|
||||||
'proposal-class-properties',
|
"proposal-class-properties",
|
||||||
'proposal-class-static-block',
|
"proposal-class-static-block",
|
||||||
'proposal-private-property-in-object',
|
"proposal-private-property-in-object",
|
||||||
'proposal-private-methods',
|
"proposal-private-methods",
|
||||||
'proposal-numeric-separator',
|
"proposal-numeric-separator",
|
||||||
'proposal-logical-assignment-operators',
|
"proposal-logical-assignment-operators",
|
||||||
'proposal-nullish-coalescing-operator',
|
"proposal-nullish-coalescing-operator",
|
||||||
'proposal-json-strings',
|
"proposal-json-strings",
|
||||||
'proposal-optional-catch-binding',
|
"proposal-optional-catch-binding",
|
||||||
'transform-parameters',
|
"transform-parameters",
|
||||||
'proposal-async-generator-functions',
|
"proposal-async-generator-functions",
|
||||||
'proposal-object-rest-spread',
|
"proposal-object-rest-spread",
|
||||||
'proposal-export-namespace-from',
|
"proposal-export-namespace-from",
|
||||||
]
|
]
|
||||||
|
|
||||||
def self.plugin_transpile_paths
|
def self.plugin_transpile_paths
|
||||||
@ -33,22 +34,22 @@ class DiscourseJsProcessor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.call(input)
|
def self.call(input)
|
||||||
root_path = input[:load_path] || ''
|
root_path = input[:load_path] || ""
|
||||||
logical_path = (input[:filename] || '').sub(root_path, '').gsub(/\.(js|es6).*$/, '').sub(/^\//, '')
|
logical_path =
|
||||||
|
(input[:filename] || "").sub(root_path, "").gsub(/\.(js|es6).*$/, "").sub(%r{^/}, "")
|
||||||
data = input[:data]
|
data = input[:data]
|
||||||
|
|
||||||
if should_transpile?(input[:filename])
|
data = transpile(data, root_path, logical_path) if should_transpile?(input[:filename])
|
||||||
data = transpile(data, root_path, logical_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
# add sourceURL until we can do proper source maps
|
# add sourceURL until we can do proper source maps
|
||||||
if !Rails.env.production? && !ember_cli?(input[:filename])
|
if !Rails.env.production? && !ember_cli?(input[:filename])
|
||||||
plugin_name = root_path[/\/plugins\/([\w-]+)\/assets/, 1]
|
plugin_name = root_path[%r{/plugins/([\w-]+)/assets}, 1]
|
||||||
source_url = if plugin_name
|
source_url =
|
||||||
"plugins/#{plugin_name}/assets/javascripts/#{logical_path}"
|
if plugin_name
|
||||||
else
|
"plugins/#{plugin_name}/assets/javascripts/#{logical_path}"
|
||||||
logical_path
|
else
|
||||||
end
|
logical_path
|
||||||
|
end
|
||||||
|
|
||||||
data = "eval(#{data.inspect} + \"\\n//# sourceURL=#{source_url}\");\n"
|
data = "eval(#{data.inspect} + \"\\n//# sourceURL=#{source_url}\");\n"
|
||||||
end
|
end
|
||||||
@ -62,7 +63,7 @@ class DiscourseJsProcessor
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.should_transpile?(filename)
|
def self.should_transpile?(filename)
|
||||||
filename ||= ''
|
filename ||= ""
|
||||||
|
|
||||||
# skip ember cli
|
# skip ember cli
|
||||||
return false if ember_cli?(filename)
|
return false if ember_cli?(filename)
|
||||||
@ -73,7 +74,7 @@ class DiscourseJsProcessor
|
|||||||
# For .js check the path...
|
# For .js check the path...
|
||||||
return false unless filename.end_with?(".js") || filename.end_with?(".js.erb")
|
return false unless filename.end_with?(".js") || filename.end_with?(".js.erb")
|
||||||
|
|
||||||
relative_path = filename.sub(Rails.root.to_s, '').sub(/^\/*/, '')
|
relative_path = filename.sub(Rails.root.to_s, "").sub(%r{^/*}, "")
|
||||||
|
|
||||||
js_root = "app/assets/javascripts"
|
js_root = "app/assets/javascripts"
|
||||||
test_root = "test/javascripts"
|
test_root = "test/javascripts"
|
||||||
@ -81,26 +82,27 @@ class DiscourseJsProcessor
|
|||||||
return false if relative_path.start_with?("#{js_root}/locales/")
|
return false if relative_path.start_with?("#{js_root}/locales/")
|
||||||
return false if relative_path.start_with?("#{js_root}/plugins/")
|
return false if relative_path.start_with?("#{js_root}/plugins/")
|
||||||
|
|
||||||
return true if %w(
|
if %w[
|
||||||
start-discourse
|
start-discourse
|
||||||
onpopstate-handler
|
onpopstate-handler
|
||||||
google-tag-manager
|
google-tag-manager
|
||||||
google-universal-analytics-v3
|
google-universal-analytics-v3
|
||||||
google-universal-analytics-v4
|
google-universal-analytics-v4
|
||||||
activate-account
|
activate-account
|
||||||
auto-redirect
|
auto-redirect
|
||||||
embed-application
|
embed-application
|
||||||
app-boot
|
app-boot
|
||||||
).any? { |f| relative_path == "#{js_root}/#{f}.js" }
|
].any? { |f| relative_path == "#{js_root}/#{f}.js" }
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
return true if plugin_transpile_paths.any? { |prefix| relative_path.start_with?(prefix) }
|
return true if plugin_transpile_paths.any? { |prefix| relative_path.start_with?(prefix) }
|
||||||
|
|
||||||
!!(relative_path =~ /^#{js_root}\/[^\/]+\// ||
|
!!(relative_path =~ %r{^#{js_root}/[^/]+/} || relative_path =~ %r{^#{test_root}/[^/]+/})
|
||||||
relative_path =~ /^#{test_root}\/[^\/]+\//)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.skip_module?(data)
|
def self.skip_module?(data)
|
||||||
!!(data.present? && data =~ /^\/\/ discourse-skip-module$/)
|
!!(data.present? && data =~ %r{^// discourse-skip-module$})
|
||||||
end
|
end
|
||||||
|
|
||||||
class Transpiler
|
class Transpiler
|
||||||
@ -113,19 +115,17 @@ class DiscourseJsProcessor
|
|||||||
|
|
||||||
def self.load_file_in_context(ctx, path, wrap_in_module: nil)
|
def self.load_file_in_context(ctx, path, wrap_in_module: nil)
|
||||||
contents = File.read("#{Rails.root}/app/assets/javascripts/#{path}")
|
contents = File.read("#{Rails.root}/app/assets/javascripts/#{path}")
|
||||||
if wrap_in_module
|
contents = <<~JS if wrap_in_module
|
||||||
contents = <<~JS
|
|
||||||
define(#{wrap_in_module.to_json}, ["exports", "require", "module"], function(exports, require, module){
|
define(#{wrap_in_module.to_json}, ["exports", "require", "module"], function(exports, require, module){
|
||||||
#{contents}
|
#{contents}
|
||||||
});
|
});
|
||||||
JS
|
JS
|
||||||
end
|
|
||||||
ctx.eval(contents, filename: path)
|
ctx.eval(contents, filename: path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create_new_context
|
def self.create_new_context
|
||||||
# timeout any eval that takes longer than 15 seconds
|
# timeout any eval that takes longer than 15 seconds
|
||||||
ctx = MiniRacer::Context.new(timeout: 15000, ensure_gc_after_idle: 2000)
|
ctx = MiniRacer::Context.new(timeout: 15_000, ensure_gc_after_idle: 2000)
|
||||||
|
|
||||||
# General shims
|
# General shims
|
||||||
ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
|
ctx.attach("rails.logger.info", proc { |err| Rails.logger.info(err.to_s) })
|
||||||
@ -158,10 +158,26 @@ class DiscourseJsProcessor
|
|||||||
|
|
||||||
# Template Compiler
|
# Template Compiler
|
||||||
load_file_in_context(ctx, "node_modules/ember-source/dist/ember-template-compiler.js")
|
load_file_in_context(ctx, "node_modules/ember-source/dist/ember-template-compiler.js")
|
||||||
load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/plugin.js", wrap_in_module: "babel-plugin-ember-template-compilation/index")
|
load_file_in_context(
|
||||||
load_file_in_context(ctx, "node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js", wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser")
|
ctx,
|
||||||
load_file_in_context(ctx, "node_modules/babel-import-util/src/index.js", wrap_in_module: "babel-import-util")
|
"node_modules/babel-plugin-ember-template-compilation/src/plugin.js",
|
||||||
load_file_in_context(ctx, "node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js", wrap_in_module: "colocated-babel-plugin")
|
wrap_in_module: "babel-plugin-ember-template-compilation/index",
|
||||||
|
)
|
||||||
|
load_file_in_context(
|
||||||
|
ctx,
|
||||||
|
"node_modules/babel-plugin-ember-template-compilation/src/expression-parser.js",
|
||||||
|
wrap_in_module: "babel-plugin-ember-template-compilation/expression-parser",
|
||||||
|
)
|
||||||
|
load_file_in_context(
|
||||||
|
ctx,
|
||||||
|
"node_modules/babel-import-util/src/index.js",
|
||||||
|
wrap_in_module: "babel-import-util",
|
||||||
|
)
|
||||||
|
load_file_in_context(
|
||||||
|
ctx,
|
||||||
|
"node_modules/ember-cli-htmlbars/lib/colocated-babel-plugin.js",
|
||||||
|
wrap_in_module: "colocated-babel-plugin",
|
||||||
|
)
|
||||||
|
|
||||||
# Widget HBS compiler
|
# Widget HBS compiler
|
||||||
widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js")
|
widget_hbs_compiler_source = File.read("#{Rails.root}/lib/javascripts/widget-hbs-compiler.js")
|
||||||
@ -170,32 +186,44 @@ class DiscourseJsProcessor
|
|||||||
#{widget_hbs_compiler_source}
|
#{widget_hbs_compiler_source}
|
||||||
});
|
});
|
||||||
JS
|
JS
|
||||||
widget_hbs_compiler_transpiled = ctx.call("rawBabelTransform", widget_hbs_compiler_source, {
|
widget_hbs_compiler_transpiled =
|
||||||
ast: false,
|
ctx.call(
|
||||||
moduleId: 'widget-hbs-compiler',
|
"rawBabelTransform",
|
||||||
plugins: DISCOURSE_COMMON_BABEL_PLUGINS
|
widget_hbs_compiler_source,
|
||||||
})
|
{ ast: false, moduleId: "widget-hbs-compiler", plugins: DISCOURSE_COMMON_BABEL_PLUGINS },
|
||||||
|
)
|
||||||
ctx.eval(widget_hbs_compiler_transpiled, filename: "widget-hbs-compiler.js")
|
ctx.eval(widget_hbs_compiler_transpiled, filename: "widget-hbs-compiler.js")
|
||||||
|
|
||||||
# Raw HBS compiler
|
# Raw HBS compiler
|
||||||
load_file_in_context(ctx, "node_modules/handlebars/dist/handlebars.js", wrap_in_module: "handlebars")
|
load_file_in_context(
|
||||||
|
ctx,
|
||||||
raw_hbs_transpiled = ctx.call(
|
"node_modules/handlebars/dist/handlebars.js",
|
||||||
"rawBabelTransform",
|
wrap_in_module: "handlebars",
|
||||||
File.read("#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js"),
|
|
||||||
{
|
|
||||||
ast: false,
|
|
||||||
moduleId: "raw-handlebars",
|
|
||||||
plugins: [
|
|
||||||
['transform-modules-amd', { noInterop: true }],
|
|
||||||
*DISCOURSE_COMMON_BABEL_PLUGINS
|
|
||||||
]
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
raw_hbs_transpiled =
|
||||||
|
ctx.call(
|
||||||
|
"rawBabelTransform",
|
||||||
|
File.read(
|
||||||
|
"#{Rails.root}/app/assets/javascripts/discourse-common/addon/lib/raw-handlebars.js",
|
||||||
|
),
|
||||||
|
{
|
||||||
|
ast: false,
|
||||||
|
moduleId: "raw-handlebars",
|
||||||
|
plugins: [
|
||||||
|
["transform-modules-amd", { noInterop: true }],
|
||||||
|
*DISCOURSE_COMMON_BABEL_PLUGINS,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
ctx.eval(raw_hbs_transpiled, filename: "raw-handlebars.js")
|
ctx.eval(raw_hbs_transpiled, filename: "raw-handlebars.js")
|
||||||
|
|
||||||
# Theme template AST transformation plugins
|
# Theme template AST transformation plugins
|
||||||
load_file_in_context(ctx, "discourse-js-processor.js", wrap_in_module: "discourse-js-processor")
|
load_file_in_context(
|
||||||
|
ctx,
|
||||||
|
"discourse-js-processor.js",
|
||||||
|
wrap_in_module: "discourse-js-processor",
|
||||||
|
)
|
||||||
|
|
||||||
# Make interfaces available via `v8.call`
|
# Make interfaces available via `v8.call`
|
||||||
ctx.eval <<~JS
|
ctx.eval <<~JS
|
||||||
@ -262,10 +290,10 @@ class DiscourseJsProcessor
|
|||||||
{
|
{
|
||||||
skip_module: @skip_module,
|
skip_module: @skip_module,
|
||||||
moduleId: module_name(root_path, logical_path),
|
moduleId: module_name(root_path, logical_path),
|
||||||
filename: logical_path || 'unknown',
|
filename: logical_path || "unknown",
|
||||||
themeId: theme_id,
|
themeId: theme_id,
|
||||||
commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS
|
commonPlugins: DISCOURSE_COMMON_BABEL_PLUGINS,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -274,15 +302,16 @@ class DiscourseJsProcessor
|
|||||||
|
|
||||||
root_base = File.basename(Rails.root)
|
root_base = File.basename(Rails.root)
|
||||||
# If the resource is a plugin, use the plugin name as a prefix
|
# If the resource is a plugin, use the plugin name as a prefix
|
||||||
if root_path =~ /(.*\/#{root_base}\/plugins\/[^\/]+)\//
|
if root_path =~ %r{(.*/#{root_base}/plugins/[^/]+)/}
|
||||||
plugin_path = "#{Regexp.last_match[1]}/plugin.rb"
|
plugin_path = "#{Regexp.last_match[1]}/plugin.rb"
|
||||||
|
|
||||||
plugin = Discourse.plugins.find { |p| p.path == plugin_path }
|
plugin = Discourse.plugins.find { |p| p.path == plugin_path }
|
||||||
path = "discourse/plugins/#{plugin.name}/#{logical_path.sub(/javascripts\//, '')}" if plugin
|
path =
|
||||||
|
"discourse/plugins/#{plugin.name}/#{logical_path.sub(%r{javascripts/}, "")}" if plugin
|
||||||
end
|
end
|
||||||
|
|
||||||
# We need to strip the app subdirectory to replicate how ember-cli works.
|
# We need to strip the app subdirectory to replicate how ember-cli works.
|
||||||
path || logical_path&.gsub('app/', '')&.gsub('addon/', '')&.gsub('admin/addon', 'admin')
|
path || logical_path&.gsub("app/", "")&.gsub("addon/", "")&.gsub("admin/addon", "admin")
|
||||||
end
|
end
|
||||||
|
|
||||||
def compile_raw_template(source, theme_id: nil)
|
def compile_raw_template(source, theme_id: nil)
|
||||||
|
@ -1,27 +1,28 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'logstash-logger'
|
require "logstash-logger"
|
||||||
|
|
||||||
class DiscourseLogstashLogger
|
class DiscourseLogstashLogger
|
||||||
def self.logger(uri:, type:)
|
def self.logger(uri:, type:)
|
||||||
# See Discourse.os_hostname
|
# See Discourse.os_hostname
|
||||||
hostname = begin
|
hostname =
|
||||||
require 'socket'
|
begin
|
||||||
Socket.gethostname
|
require "socket"
|
||||||
rescue => e
|
Socket.gethostname
|
||||||
`hostname`.chomp
|
rescue => e
|
||||||
end
|
`hostname`.chomp
|
||||||
|
end
|
||||||
|
|
||||||
LogStashLogger.new(
|
LogStashLogger.new(
|
||||||
uri: uri,
|
uri: uri,
|
||||||
sync: true,
|
sync: true,
|
||||||
customize_event: ->(event) {
|
customize_event: ->(event) do
|
||||||
event['hostname'] = hostname
|
event["hostname"] = hostname
|
||||||
event['severity_name'] = event['severity']
|
event["severity_name"] = event["severity"]
|
||||||
event['severity'] = Object.const_get("Logger::Severity::#{event['severity']}")
|
event["severity"] = Object.const_get("Logger::Severity::#{event["severity"]}")
|
||||||
event['type'] = type
|
event["type"] = type
|
||||||
event['pid'] = Process.pid
|
event["pid"] = Process.pid
|
||||||
},
|
end,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
# A class that handles interaction between a plugin and the Discourse App.
|
# A class that handles interaction between a plugin and the Discourse App.
|
||||||
#
|
#
|
||||||
class DiscoursePluginRegistry
|
class DiscoursePluginRegistry
|
||||||
|
|
||||||
# Plugins often need to be able to register additional handlers, data, or
|
# Plugins often need to be able to register additional handlers, data, or
|
||||||
# classes that will be used by core classes. This should be used if you
|
# classes that will be used by core classes. This should be used if you
|
||||||
# need to control which type the registry is, and if it doesn't need to
|
# need to control which type the registry is, and if it doesn't need to
|
||||||
@ -24,9 +23,7 @@ class DiscoursePluginRegistry
|
|||||||
instance_variable_set(:"@#{register_name}", type.new)
|
instance_variable_set(:"@#{register_name}", type.new)
|
||||||
end
|
end
|
||||||
|
|
||||||
define_method(register_name) do
|
define_method(register_name) { self.class.public_send(register_name) }
|
||||||
self.class.public_send(register_name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Plugins often need to add values to a list, and we need to filter those
|
# Plugins often need to add values to a list, and we need to filter those
|
||||||
@ -45,10 +42,7 @@ class DiscoursePluginRegistry
|
|||||||
|
|
||||||
define_singleton_method(register_name) do
|
define_singleton_method(register_name) do
|
||||||
unfiltered = public_send(:"_raw_#{register_name}")
|
unfiltered = public_send(:"_raw_#{register_name}")
|
||||||
unfiltered
|
unfiltered.filter { |v| v[:plugin].enabled? }.map { |v| v[:value] }.uniq
|
||||||
.filter { |v| v[:plugin].enabled? }
|
|
||||||
.map { |v| v[:value] }
|
|
||||||
.uniq
|
|
||||||
end
|
end
|
||||||
|
|
||||||
define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin|
|
define_singleton_method("register_#{register_name.to_s.singularize}") do |value, plugin|
|
||||||
@ -158,9 +152,7 @@ class DiscoursePluginRegistry
|
|||||||
next if each_options[:admin]
|
next if each_options[:admin]
|
||||||
end
|
end
|
||||||
|
|
||||||
Dir.glob("#{root}/**/*.#{ext}") do |f|
|
Dir.glob("#{root}/**/*.#{ext}") { |f| yield f }
|
||||||
yield f
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -227,7 +219,7 @@ class DiscoursePluginRegistry
|
|||||||
|
|
||||||
def self.seed_paths
|
def self.seed_paths
|
||||||
result = SeedFu.fixture_paths.dup
|
result = SeedFu.fixture_paths.dup
|
||||||
unless Rails.env.test? && ENV['LOAD_PLUGINS'] != "1"
|
unless Rails.env.test? && ENV["LOAD_PLUGINS"] != "1"
|
||||||
seed_path_builders.each { |b| result += b.call }
|
seed_path_builders.each { |b| result += b.call }
|
||||||
end
|
end
|
||||||
result.uniq
|
result.uniq
|
||||||
@ -239,7 +231,7 @@ class DiscoursePluginRegistry
|
|||||||
|
|
||||||
VENDORED_CORE_PRETTY_TEXT_MAP = {
|
VENDORED_CORE_PRETTY_TEXT_MAP = {
|
||||||
"moment.js" => "vendor/assets/javascripts/moment.js",
|
"moment.js" => "vendor/assets/javascripts/moment.js",
|
||||||
"moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js"
|
"moment-timezone.js" => "vendor/assets/javascripts/moment-timezone-with-data.js",
|
||||||
}
|
}
|
||||||
def self.core_asset_for_name(name)
|
def self.core_asset_for_name(name)
|
||||||
asset = VENDORED_CORE_PRETTY_TEXT_MAP[name]
|
asset = VENDORED_CORE_PRETTY_TEXT_MAP[name]
|
||||||
@ -248,16 +240,12 @@ class DiscoursePluginRegistry
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.reset!
|
def self.reset!
|
||||||
@@register_names.each do |name|
|
@@register_names.each { |name| instance_variable_set(:"@#{name}", nil) }
|
||||||
instance_variable_set(:"@#{name}", nil)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.reset_register!(register_name)
|
def self.reset_register!(register_name)
|
||||||
found_register = @@register_names.detect { |name| name == register_name }
|
found_register = @@register_names.detect { |name| name == register_name }
|
||||||
|
|
||||||
if found_register
|
instance_variable_set(:"@#{found_register}", nil) if found_register
|
||||||
instance_variable_set(:"@#{found_register}", nil)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -46,15 +46,103 @@ class DiscourseRedis
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Proxy key methods through, but prefix the keys with the namespace
|
# Proxy key methods through, but prefix the keys with the namespace
|
||||||
[:append, :blpop, :brpop, :brpoplpush, :decr, :decrby, :expire, :expireat, :get, :getbit, :getrange, :getset,
|
%i[
|
||||||
:hdel, :hexists, :hget, :hgetall, :hincrby, :hincrbyfloat, :hkeys, :hlen, :hmget, :hmset, :hset, :hsetnx, :hvals, :incr,
|
append
|
||||||
:incrby, :incrbyfloat, :lindex, :linsert, :llen, :lpop, :lpush, :lpushx, :lrange, :lrem, :lset, :ltrim,
|
blpop
|
||||||
:mapped_hmset, :mapped_hmget, :mapped_mget, :mapped_mset, :mapped_msetnx, :move, :mset,
|
brpop
|
||||||
:msetnx, :persist, :pexpire, :pexpireat, :psetex, :pttl, :rename, :renamenx, :rpop, :rpoplpush, :rpush, :rpushx, :sadd, :sadd?, :scard,
|
brpoplpush
|
||||||
:sdiff, :set, :setbit, :setex, :setnx, :setrange, :sinter, :sismember, :smembers, :sort, :spop, :srandmember, :srem, :srem?, :strlen,
|
decr
|
||||||
:sunion, :ttl, :type, :watch, :zadd, :zcard, :zcount, :zincrby, :zrange, :zrangebyscore, :zrank, :zrem, :zremrangebyrank,
|
decrby
|
||||||
:zremrangebyscore, :zrevrange, :zrevrangebyscore, :zrevrank, :zrangebyscore,
|
expire
|
||||||
:dump, :restore].each do |m|
|
expireat
|
||||||
|
get
|
||||||
|
getbit
|
||||||
|
getrange
|
||||||
|
getset
|
||||||
|
hdel
|
||||||
|
hexists
|
||||||
|
hget
|
||||||
|
hgetall
|
||||||
|
hincrby
|
||||||
|
hincrbyfloat
|
||||||
|
hkeys
|
||||||
|
hlen
|
||||||
|
hmget
|
||||||
|
hmset
|
||||||
|
hset
|
||||||
|
hsetnx
|
||||||
|
hvals
|
||||||
|
incr
|
||||||
|
incrby
|
||||||
|
incrbyfloat
|
||||||
|
lindex
|
||||||
|
linsert
|
||||||
|
llen
|
||||||
|
lpop
|
||||||
|
lpush
|
||||||
|
lpushx
|
||||||
|
lrange
|
||||||
|
lrem
|
||||||
|
lset
|
||||||
|
ltrim
|
||||||
|
mapped_hmset
|
||||||
|
mapped_hmget
|
||||||
|
mapped_mget
|
||||||
|
mapped_mset
|
||||||
|
mapped_msetnx
|
||||||
|
move
|
||||||
|
mset
|
||||||
|
msetnx
|
||||||
|
persist
|
||||||
|
pexpire
|
||||||
|
pexpireat
|
||||||
|
psetex
|
||||||
|
pttl
|
||||||
|
rename
|
||||||
|
renamenx
|
||||||
|
rpop
|
||||||
|
rpoplpush
|
||||||
|
rpush
|
||||||
|
rpushx
|
||||||
|
sadd
|
||||||
|
sadd?
|
||||||
|
scard
|
||||||
|
sdiff
|
||||||
|
set
|
||||||
|
setbit
|
||||||
|
setex
|
||||||
|
setnx
|
||||||
|
setrange
|
||||||
|
sinter
|
||||||
|
sismember
|
||||||
|
smembers
|
||||||
|
sort
|
||||||
|
spop
|
||||||
|
srandmember
|
||||||
|
srem
|
||||||
|
srem?
|
||||||
|
strlen
|
||||||
|
sunion
|
||||||
|
ttl
|
||||||
|
type
|
||||||
|
watch
|
||||||
|
zadd
|
||||||
|
zcard
|
||||||
|
zcount
|
||||||
|
zincrby
|
||||||
|
zrange
|
||||||
|
zrangebyscore
|
||||||
|
zrank
|
||||||
|
zrem
|
||||||
|
zremrangebyrank
|
||||||
|
zremrangebyscore
|
||||||
|
zrevrange
|
||||||
|
zrevrangebyscore
|
||||||
|
zrevrank
|
||||||
|
zrangebyscore
|
||||||
|
dump
|
||||||
|
restore
|
||||||
|
].each do |m|
|
||||||
define_method m do |*args, **kwargs|
|
define_method m do |*args, **kwargs|
|
||||||
args[0] = "#{namespace}:#{args[0]}" if @namespace
|
args[0] = "#{namespace}:#{args[0]}" if @namespace
|
||||||
DiscourseRedis.ignore_readonly { @redis.public_send(m, *args, **kwargs) }
|
DiscourseRedis.ignore_readonly { @redis.public_send(m, *args, **kwargs) }
|
||||||
@ -72,7 +160,7 @@ class DiscourseRedis
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mget(*args)
|
def mget(*args)
|
||||||
args.map! { |a| "#{namespace}:#{a}" } if @namespace
|
args.map! { |a| "#{namespace}:#{a}" } if @namespace
|
||||||
DiscourseRedis.ignore_readonly { @redis.mget(*args) }
|
DiscourseRedis.ignore_readonly { @redis.mget(*args) }
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -86,14 +174,13 @@ class DiscourseRedis
|
|||||||
|
|
||||||
def scan_each(options = {}, &block)
|
def scan_each(options = {}, &block)
|
||||||
DiscourseRedis.ignore_readonly do
|
DiscourseRedis.ignore_readonly do
|
||||||
match = options[:match].presence || '*'
|
match = options[:match].presence || "*"
|
||||||
|
|
||||||
options[:match] =
|
options[:match] = if @namespace
|
||||||
if @namespace
|
"#{namespace}:#{match}"
|
||||||
"#{namespace}:#{match}"
|
else
|
||||||
else
|
match
|
||||||
match
|
end
|
||||||
end
|
|
||||||
|
|
||||||
if block
|
if block
|
||||||
@redis.scan_each(**options) do |key|
|
@redis.scan_each(**options) do |key|
|
||||||
@ -101,17 +188,19 @@ class DiscourseRedis
|
|||||||
block.call(key)
|
block.call(key)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@redis.scan_each(**options).map do |key|
|
@redis
|
||||||
key = remove_namespace(key) if @namespace
|
.scan_each(**options)
|
||||||
key
|
.map do |key|
|
||||||
end
|
key = remove_namespace(key) if @namespace
|
||||||
|
key
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def keys(pattern = nil)
|
def keys(pattern = nil)
|
||||||
DiscourseRedis.ignore_readonly do
|
DiscourseRedis.ignore_readonly do
|
||||||
pattern = pattern || '*'
|
pattern = pattern || "*"
|
||||||
pattern = "#{namespace}:#{pattern}" if @namespace
|
pattern = "#{namespace}:#{pattern}" if @namespace
|
||||||
keys = @redis.keys(pattern)
|
keys = @redis.keys(pattern)
|
||||||
|
|
||||||
@ -125,9 +214,7 @@ class DiscourseRedis
|
|||||||
end
|
end
|
||||||
|
|
||||||
def delete_prefixed(prefix)
|
def delete_prefixed(prefix)
|
||||||
DiscourseRedis.ignore_readonly do
|
DiscourseRedis.ignore_readonly { keys("#{prefix}*").each { |k| Discourse.redis.del(k) } }
|
||||||
keys("#{prefix}*").each { |k| Discourse.redis.del(k) }
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def reconnect
|
def reconnect
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
class DiscourseSourcemappingUrlProcessor < Sprockets::Rails::SourcemappingUrlProcessor
|
class DiscourseSourcemappingUrlProcessor < Sprockets::Rails::SourcemappingUrlProcessor
|
||||||
def self.sourcemap_asset_path(sourcemap_logical_path, context:)
|
def self.sourcemap_asset_path(sourcemap_logical_path, context:)
|
||||||
result = super(sourcemap_logical_path, context: context)
|
result = super(sourcemap_logical_path, context: context)
|
||||||
if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) || sourcemap_logical_path.start_with?("plugins/")
|
if (File.basename(sourcemap_logical_path) === sourcemap_logical_path) ||
|
||||||
|
sourcemap_logical_path.start_with?("plugins/")
|
||||||
# If the original sourcemap reference is relative, keep it relative
|
# If the original sourcemap reference is relative, keep it relative
|
||||||
result = File.basename(result)
|
result = File.basename(result)
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DiscourseTagging
|
module DiscourseTagging
|
||||||
|
|
||||||
TAGS_FIELD_NAME ||= "tags"
|
TAGS_FIELD_NAME ||= "tags"
|
||||||
TAGS_FILTER_REGEXP ||= /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<>
|
TAGS_FILTER_REGEXP ||= /[\/\?#\[\]@!\$&'\(\)\*\+,;=\.%\\`^\s|\{\}"<>]+/ # /?#[]@!$&'()*+,;=.%\`^|{}"<>
|
||||||
TAGS_STAFF_CACHE_KEY ||= "staff_tag_names"
|
TAGS_STAFF_CACHE_KEY ||= "staff_tag_names"
|
||||||
@ -22,9 +21,11 @@ module DiscourseTagging
|
|||||||
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || []
|
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, guardian) || []
|
||||||
|
|
||||||
if !tag_names.empty?
|
if !tag_names.empty?
|
||||||
Tag.where_name(tag_names).joins(:target_tag).includes(:target_tag).each do |tag|
|
Tag
|
||||||
tag_names[tag_names.index(tag.name)] = tag.target_tag.name
|
.where_name(tag_names)
|
||||||
end
|
.joins(:target_tag)
|
||||||
|
.includes(:target_tag)
|
||||||
|
.each { |tag| tag_names[tag_names.index(tag.name)] = tag.target_tag.name }
|
||||||
end
|
end
|
||||||
|
|
||||||
# tags currently on the topic
|
# tags currently on the topic
|
||||||
@ -45,9 +46,7 @@ module DiscourseTagging
|
|||||||
# If this user has explicit permission to use certain tags,
|
# If this user has explicit permission to use certain tags,
|
||||||
# we need to ensure those tags are removed from the list of
|
# we need to ensure those tags are removed from the list of
|
||||||
# restricted tags
|
# restricted tags
|
||||||
if permitted_tags.present?
|
readonly_tags = readonly_tags - permitted_tags if permitted_tags.present?
|
||||||
readonly_tags = readonly_tags - permitted_tags
|
|
||||||
end
|
|
||||||
|
|
||||||
# visible, but not usable, tags this user is trying to use
|
# visible, but not usable, tags this user is trying to use
|
||||||
disallowed_tags = new_tag_names & readonly_tags
|
disallowed_tags = new_tag_names & readonly_tags
|
||||||
@ -55,13 +54,19 @@ module DiscourseTagging
|
|||||||
disallowed_tags += new_tag_names & hidden_tags
|
disallowed_tags += new_tag_names & hidden_tags
|
||||||
|
|
||||||
if disallowed_tags.present?
|
if disallowed_tags.present?
|
||||||
topic.errors.add(:base, I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" ")))
|
topic.errors.add(
|
||||||
|
:base,
|
||||||
|
I18n.t("tags.restricted_tag_disallowed", tag: disallowed_tags.join(" ")),
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
removed_readonly_tags = removed_tag_names & readonly_tags
|
removed_readonly_tags = removed_tag_names & readonly_tags
|
||||||
if removed_readonly_tags.present?
|
if removed_readonly_tags.present?
|
||||||
topic.errors.add(:base, I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" ")))
|
topic.errors.add(
|
||||||
|
:base,
|
||||||
|
I18n.t("tags.restricted_tag_remove_disallowed", tag: removed_readonly_tags.join(" ")),
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -73,50 +78,61 @@ module DiscourseTagging
|
|||||||
if tag_names.present?
|
if tag_names.present?
|
||||||
# guardian is explicitly nil cause we don't want to strip all
|
# guardian is explicitly nil cause we don't want to strip all
|
||||||
# staff tags that already passed validation
|
# staff tags that already passed validation
|
||||||
tags = filter_allowed_tags(
|
tags =
|
||||||
nil, # guardian
|
filter_allowed_tags(
|
||||||
for_topic: true,
|
nil, # guardian
|
||||||
category: category,
|
for_topic: true,
|
||||||
selected_tags: tag_names,
|
category: category,
|
||||||
only_tag_names: tag_names
|
selected_tags: tag_names,
|
||||||
)
|
only_tag_names: tag_names,
|
||||||
|
)
|
||||||
|
|
||||||
# keep existent tags that current user cannot use
|
# keep existent tags that current user cannot use
|
||||||
tags += Tag.where(name: old_tag_names & tag_names)
|
tags += Tag.where(name: old_tag_names & tag_names)
|
||||||
|
|
||||||
tags = Tag.where(id: tags.map(&:id)).all.to_a if tags.size > 0
|
tags = Tag.where(id: tags.map(&:id)).all.to_a if tags.size > 0
|
||||||
|
|
||||||
if tags.size < tag_names.size && (category.nil? || category.allow_global_tags || (category.tags.count == 0 && category.tag_groups.count == 0))
|
if tags.size < tag_names.size &&
|
||||||
|
(
|
||||||
|
category.nil? || category.allow_global_tags ||
|
||||||
|
(category.tags.count == 0 && category.tag_groups.count == 0)
|
||||||
|
)
|
||||||
tag_names.each do |name|
|
tag_names.each do |name|
|
||||||
unless Tag.where_name(name).exists?
|
tags << Tag.create(name: name) unless Tag.where_name(name).exists?
|
||||||
tags << Tag.create(name: name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# add missing mandatory parent tags
|
# add missing mandatory parent tags
|
||||||
tag_ids = tags.map(&:id)
|
tag_ids = tags.map(&:id)
|
||||||
|
|
||||||
parent_tags_map = DB.query("
|
parent_tags_map =
|
||||||
|
DB
|
||||||
|
.query(
|
||||||
|
"
|
||||||
SELECT tgm.tag_id, tg.parent_tag_id
|
SELECT tgm.tag_id, tg.parent_tag_id
|
||||||
FROM tag_groups tg
|
FROM tag_groups tg
|
||||||
INNER JOIN tag_group_memberships tgm
|
INNER JOIN tag_group_memberships tgm
|
||||||
ON tgm.tag_group_id = tg.id
|
ON tgm.tag_group_id = tg.id
|
||||||
WHERE tg.parent_tag_id IS NOT NULL
|
WHERE tg.parent_tag_id IS NOT NULL
|
||||||
AND tgm.tag_id IN (?)
|
AND tgm.tag_id IN (?)
|
||||||
", tag_ids).inject({}) do |h, v|
|
",
|
||||||
h[v.tag_id] ||= []
|
tag_ids,
|
||||||
h[v.tag_id] << v.parent_tag_id
|
)
|
||||||
h
|
.inject({}) do |h, v|
|
||||||
end
|
h[v.tag_id] ||= []
|
||||||
|
h[v.tag_id] << v.parent_tag_id
|
||||||
|
h
|
||||||
|
end
|
||||||
|
|
||||||
missing_parent_tag_ids = parent_tags_map.map do |_, parent_tag_ids|
|
missing_parent_tag_ids =
|
||||||
(tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil
|
parent_tags_map
|
||||||
end.compact.uniq
|
.map do |_, parent_tag_ids|
|
||||||
|
(tag_ids & parent_tag_ids).size == 0 ? parent_tag_ids.first : nil
|
||||||
|
end
|
||||||
|
.compact
|
||||||
|
.uniq
|
||||||
|
|
||||||
unless missing_parent_tag_ids.empty?
|
tags = tags + Tag.where(id: missing_parent_tag_ids).all unless missing_parent_tag_ids.empty?
|
||||||
tags = tags + Tag.where(id: missing_parent_tag_ids).all
|
|
||||||
end
|
|
||||||
|
|
||||||
return false unless validate_min_required_tags_for_category(guardian, topic, category, tags)
|
return false unless validate_min_required_tags_for_category(guardian, topic, category, tags)
|
||||||
return false unless validate_required_tags_from_group(guardian, topic, category, tags)
|
return false unless validate_required_tags_from_group(guardian, topic, category, tags)
|
||||||
@ -137,7 +153,9 @@ module DiscourseTagging
|
|||||||
|
|
||||||
DiscourseEvent.trigger(
|
DiscourseEvent.trigger(
|
||||||
:topic_tags_changed,
|
:topic_tags_changed,
|
||||||
topic, old_tag_names: old_tag_names, new_tag_names: topic.tags.map(&:name)
|
topic,
|
||||||
|
old_tag_names: old_tag_names,
|
||||||
|
new_tag_names: topic.tags.map(&:name),
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -146,12 +164,12 @@ module DiscourseTagging
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.validate_min_required_tags_for_category(guardian, model, category, tags = [])
|
def self.validate_min_required_tags_for_category(guardian, model, category, tags = [])
|
||||||
if !guardian.is_staff? &&
|
if !guardian.is_staff? && category && category.minimum_required_tags > 0 &&
|
||||||
category &&
|
tags.length < category.minimum_required_tags
|
||||||
category.minimum_required_tags > 0 &&
|
model.errors.add(
|
||||||
tags.length < category.minimum_required_tags
|
:base,
|
||||||
|
I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags),
|
||||||
model.errors.add(:base, I18n.t("tags.minimum_required_tags", count: category.minimum_required_tags))
|
)
|
||||||
false
|
false
|
||||||
else
|
else
|
||||||
true
|
true
|
||||||
@ -164,17 +182,17 @@ module DiscourseTagging
|
|||||||
success = true
|
success = true
|
||||||
category.category_required_tag_groups.each do |crtg|
|
category.category_required_tag_groups.each do |crtg|
|
||||||
if tags.length < crtg.min_count ||
|
if tags.length < crtg.min_count ||
|
||||||
crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count
|
crtg.tag_group.tags.where("tags.id in (?)", tags.map(&:id)).count < crtg.min_count
|
||||||
|
|
||||||
success = false
|
success = false
|
||||||
|
|
||||||
model.errors.add(:base,
|
model.errors.add(
|
||||||
|
:base,
|
||||||
I18n.t(
|
I18n.t(
|
||||||
"tags.required_tags_from_group",
|
"tags.required_tags_from_group",
|
||||||
count: crtg.min_count,
|
count: crtg.min_count,
|
||||||
tag_group_name: crtg.tag_group.name,
|
tag_group_name: crtg.tag_group.name,
|
||||||
tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", ")
|
tags: crtg.tag_group.tags.order(:id).pluck(:name).join(", "),
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -189,24 +207,28 @@ module DiscourseTagging
|
|||||||
tags_restricted_to_categories = Hash.new { |h, k| h[k] = Set.new }
|
tags_restricted_to_categories = Hash.new { |h, k| h[k] = Set.new }
|
||||||
|
|
||||||
query = Tag.where(name: tags)
|
query = Tag.where(name: tags)
|
||||||
query.joins(tag_groups: :categories).pluck(:name, 'categories.id').each do |(tag, cat_id)|
|
query
|
||||||
tags_restricted_to_categories[tag] << cat_id
|
.joins(tag_groups: :categories)
|
||||||
end
|
.pluck(:name, "categories.id")
|
||||||
query.joins(:categories).pluck(:name, 'categories.id').each do |(tag, cat_id)|
|
.each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id }
|
||||||
tags_restricted_to_categories[tag] << cat_id
|
query
|
||||||
end
|
.joins(:categories)
|
||||||
|
.pluck(:name, "categories.id")
|
||||||
|
.each { |(tag, cat_id)| tags_restricted_to_categories[tag] << cat_id }
|
||||||
|
|
||||||
unallowed_tags = tags_restricted_to_categories.keys.select do |tag|
|
unallowed_tags =
|
||||||
!tags_restricted_to_categories[tag].include?(category.id)
|
tags_restricted_to_categories.keys.select do |tag|
|
||||||
end
|
!tags_restricted_to_categories[tag].include?(category.id)
|
||||||
|
end
|
||||||
|
|
||||||
if unallowed_tags.present?
|
if unallowed_tags.present?
|
||||||
msg = I18n.t(
|
msg =
|
||||||
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
|
I18n.t(
|
||||||
count: unallowed_tags.size,
|
"tags.forbidden.restricted_tags_cannot_be_used_in_category",
|
||||||
tags: unallowed_tags.sort.join(", "),
|
count: unallowed_tags.size,
|
||||||
category: category.name
|
tags: unallowed_tags.sort.join(", "),
|
||||||
)
|
category: category.name,
|
||||||
|
)
|
||||||
model.errors.add(:base, msg)
|
model.errors.add(:base, msg)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -214,12 +236,13 @@ module DiscourseTagging
|
|||||||
if !category.allow_global_tags && category.has_restricted_tags?
|
if !category.allow_global_tags && category.has_restricted_tags?
|
||||||
unrestricted_tags = tags - tags_restricted_to_categories.keys
|
unrestricted_tags = tags - tags_restricted_to_categories.keys
|
||||||
if unrestricted_tags.present?
|
if unrestricted_tags.present?
|
||||||
msg = I18n.t(
|
msg =
|
||||||
"tags.forbidden.category_does_not_allow_tags",
|
I18n.t(
|
||||||
count: unrestricted_tags.size,
|
"tags.forbidden.category_does_not_allow_tags",
|
||||||
tags: unrestricted_tags.sort.join(", "),
|
count: unrestricted_tags.size,
|
||||||
category: category.name
|
tags: unrestricted_tags.sort.join(", "),
|
||||||
)
|
category: category.name,
|
||||||
|
)
|
||||||
model.errors.add(:base, msg)
|
model.errors.add(:base, msg)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
@ -280,7 +303,8 @@ module DiscourseTagging
|
|||||||
def self.filter_allowed_tags(guardian, opts = {})
|
def self.filter_allowed_tags(guardian, opts = {})
|
||||||
selected_tag_ids = opts[:selected_tags] ? Tag.where_name(opts[:selected_tags]).pluck(:id) : []
|
selected_tag_ids = opts[:selected_tags] ? Tag.where_name(opts[:selected_tags]).pluck(:id) : []
|
||||||
category = opts[:category]
|
category = opts[:category]
|
||||||
category_has_restricted_tags = category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false
|
category_has_restricted_tags =
|
||||||
|
category ? (category.tags.count > 0 || category.tag_groups.count > 0) : false
|
||||||
|
|
||||||
# If guardian is nil, it means the caller doesn't want tags to be filtered
|
# If guardian is nil, it means the caller doesn't want tags to be filtered
|
||||||
# based on guardian rules. Use the same rules as for staff users.
|
# based on guardian rules. Use the same rules as for staff users.
|
||||||
@ -288,9 +312,7 @@ module DiscourseTagging
|
|||||||
|
|
||||||
builder_params = {}
|
builder_params = {}
|
||||||
|
|
||||||
unless selected_tag_ids.empty?
|
builder_params[:selected_tag_ids] = selected_tag_ids unless selected_tag_ids.empty?
|
||||||
builder_params[:selected_tag_ids] = selected_tag_ids
|
|
||||||
end
|
|
||||||
|
|
||||||
sql = +"WITH #{TAG_GROUP_RESTRICTIONS_SQL}, #{CATEGORY_RESTRICTIONS_SQL}"
|
sql = +"WITH #{TAG_GROUP_RESTRICTIONS_SQL}, #{CATEGORY_RESTRICTIONS_SQL}"
|
||||||
if (opts[:for_input] || opts[:for_topic]) && filter_for_non_staff
|
if (opts[:for_input] || opts[:for_topic]) && filter_for_non_staff
|
||||||
@ -301,13 +323,14 @@ module DiscourseTagging
|
|||||||
|
|
||||||
outer_join = category.nil? || category.allow_global_tags || !category_has_restricted_tags
|
outer_join = category.nil? || category.allow_global_tags || !category_has_restricted_tags
|
||||||
|
|
||||||
distinct_clause = if opts[:order_popularity]
|
distinct_clause =
|
||||||
"DISTINCT ON (topic_count, name)"
|
if opts[:order_popularity]
|
||||||
elsif opts[:order_search_results] && opts[:term].present?
|
"DISTINCT ON (topic_count, name)"
|
||||||
"DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)"
|
elsif opts[:order_search_results] && opts[:term].present?
|
||||||
else
|
"DISTINCT ON (lower(name) = lower(:cleaned_term), topic_count, name)"
|
||||||
""
|
else
|
||||||
end
|
""
|
||||||
|
end
|
||||||
|
|
||||||
sql << <<~SQL
|
sql << <<~SQL
|
||||||
SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description,
|
SELECT #{distinct_clause} t.id, t.name, t.topic_count, t.pm_topic_count, t.description,
|
||||||
@ -336,16 +359,20 @@ module DiscourseTagging
|
|||||||
# parent tag requirements
|
# parent tag requirements
|
||||||
if opts[:for_input]
|
if opts[:for_input]
|
||||||
builder.where(
|
builder.where(
|
||||||
builder_params[:selected_tag_ids] ?
|
(
|
||||||
"tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)" :
|
if builder_params[:selected_tag_ids]
|
||||||
"tgm_id IS NULL OR parent_tag_id IS NULL"
|
"tgm_id IS NULL OR parent_tag_id IS NULL OR parent_tag_id IN (:selected_tag_ids)"
|
||||||
|
else
|
||||||
|
"tgm_id IS NULL OR parent_tag_id IS NULL"
|
||||||
|
end
|
||||||
|
),
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if category && category_has_restricted_tags
|
if category && category_has_restricted_tags
|
||||||
builder.where(
|
builder.where(
|
||||||
category.allow_global_tags ? "category_id = ? OR category_id IS NULL" : "category_id = ?",
|
category.allow_global_tags ? "category_id = ? OR category_id IS NULL" : "category_id = ?",
|
||||||
category.id
|
category.id,
|
||||||
)
|
)
|
||||||
elsif category || opts[:for_input] || opts[:for_topic]
|
elsif category || opts[:for_input] || opts[:for_topic]
|
||||||
# tags not restricted to any categories
|
# tags not restricted to any categories
|
||||||
@ -354,7 +381,9 @@ module DiscourseTagging
|
|||||||
|
|
||||||
if filter_for_non_staff && (opts[:for_input] || opts[:for_topic])
|
if filter_for_non_staff && (opts[:for_input] || opts[:for_topic])
|
||||||
# exclude staff-only tag groups
|
# exclude staff-only tag groups
|
||||||
builder.where("tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)")
|
builder.where(
|
||||||
|
"tag_group_id IS NULL OR tag_group_id IN (SELECT tag_group_id FROM permitted_tag_groups)",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
term = opts[:term]
|
term = opts[:term]
|
||||||
@ -380,7 +409,8 @@ module DiscourseTagging
|
|||||||
# - and no search term has been included
|
# - and no search term has been included
|
||||||
required_tag_ids = nil
|
required_tag_ids = nil
|
||||||
required_category_tag_group = nil
|
required_category_tag_group = nil
|
||||||
if opts[:for_input] && category&.category_required_tag_groups.present? && (filter_for_non_staff || term.blank?)
|
if opts[:for_input] && category&.category_required_tag_groups.present? &&
|
||||||
|
(filter_for_non_staff || term.blank?)
|
||||||
category.category_required_tag_groups.each do |crtg|
|
category.category_required_tag_groups.each do |crtg|
|
||||||
group_tags = crtg.tag_group.tags.pluck(:id)
|
group_tags = crtg.tag_group.tags.pluck(:id)
|
||||||
next if (group_tags & selected_tag_ids).size >= crtg.min_count
|
next if (group_tags & selected_tag_ids).size >= crtg.min_count
|
||||||
@ -426,22 +456,18 @@ module DiscourseTagging
|
|||||||
if !one_tag_per_group_ids.empty?
|
if !one_tag_per_group_ids.empty?
|
||||||
builder.where(
|
builder.where(
|
||||||
"tag_group_id IS NULL OR tag_group_id NOT IN (?) OR id IN (:selected_tag_ids)",
|
"tag_group_id IS NULL OR tag_group_id NOT IN (?) OR id IN (:selected_tag_ids)",
|
||||||
one_tag_per_group_ids
|
one_tag_per_group_ids,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if opts[:exclude_synonyms]
|
builder.where("target_tag_id IS NULL") if opts[:exclude_synonyms]
|
||||||
builder.where("target_tag_id IS NULL")
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts[:exclude_has_synonyms]
|
if opts[:exclude_has_synonyms]
|
||||||
builder.where("id NOT IN (SELECT target_tag_id FROM tags WHERE target_tag_id IS NOT NULL)")
|
builder.where("id NOT IN (SELECT target_tag_id FROM tags WHERE target_tag_id IS NOT NULL)")
|
||||||
end
|
end
|
||||||
|
|
||||||
if opts[:excluded_tag_names]&.any?
|
builder.where("name NOT IN (?)", opts[:excluded_tag_names]) if opts[:excluded_tag_names]&.any?
|
||||||
builder.where("name NOT IN (?)", opts[:excluded_tag_names])
|
|
||||||
end
|
|
||||||
|
|
||||||
if opts[:limit]
|
if opts[:limit]
|
||||||
if required_tag_ids && term.blank?
|
if required_tag_ids && term.blank?
|
||||||
@ -465,7 +491,7 @@ module DiscourseTagging
|
|||||||
if required_category_tag_group
|
if required_category_tag_group
|
||||||
context[:required_tag_group] = {
|
context[:required_tag_group] = {
|
||||||
name: required_category_tag_group.tag_group.name,
|
name: required_category_tag_group.tag_group.name,
|
||||||
min_count: required_category_tag_group.min_count
|
min_count: required_category_tag_group.min_count,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
[result, context]
|
[result, context]
|
||||||
@ -480,21 +506,15 @@ module DiscourseTagging
|
|||||||
else
|
else
|
||||||
# Visible tags either have no permissions or have allowable permissions
|
# Visible tags either have no permissions or have allowable permissions
|
||||||
Tag
|
Tag
|
||||||
.where.not(
|
.where.not(id: TagGroupMembership.joins(tag_group: :tag_group_permissions).select(:tag_id))
|
||||||
id:
|
|
||||||
TagGroupMembership
|
|
||||||
.joins(tag_group: :tag_group_permissions)
|
|
||||||
.select(:tag_id)
|
|
||||||
)
|
|
||||||
.or(
|
.or(
|
||||||
Tag
|
Tag.where(
|
||||||
.where(
|
id:
|
||||||
id:
|
TagGroupPermission
|
||||||
TagGroupPermission
|
.joins(tag_group: :tag_group_memberships)
|
||||||
.joins(tag_group: :tag_group_memberships)
|
.where(group_id: permitted_group_ids_query(guardian))
|
||||||
.where(group_id: permitted_group_ids_query(guardian))
|
.select("tag_group_memberships.tag_id"),
|
||||||
.select('tag_group_memberships.tag_id'),
|
),
|
||||||
)
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -509,21 +529,18 @@ module DiscourseTagging
|
|||||||
|
|
||||||
def self.permitted_group_ids_query(guardian = nil)
|
def self.permitted_group_ids_query(guardian = nil)
|
||||||
if guardian&.authenticated?
|
if guardian&.authenticated?
|
||||||
Group
|
Group.from(
|
||||||
.from(
|
Group.sanitize_sql(
|
||||||
Group.sanitize_sql(
|
[
|
||||||
["(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups", Group::AUTO_GROUPS[:everyone]]
|
"(SELECT ? AS id UNION #{guardian.user.groups.select(:id).to_sql}) as groups",
|
||||||
)
|
Group::AUTO_GROUPS[:everyone],
|
||||||
)
|
],
|
||||||
.select(:id)
|
),
|
||||||
|
).select(:id)
|
||||||
else
|
else
|
||||||
Group
|
Group.from(
|
||||||
.from(
|
Group.sanitize_sql(["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]]),
|
||||||
Group.sanitize_sql(
|
).select(:id)
|
||||||
["(SELECT ? AS id) AS groups", Group::AUTO_GROUPS[:everyone]]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.select(:id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -535,9 +552,11 @@ module DiscourseTagging
|
|||||||
def self.readonly_tag_names(guardian = nil)
|
def self.readonly_tag_names(guardian = nil)
|
||||||
return [] if guardian&.is_staff?
|
return [] if guardian&.is_staff?
|
||||||
|
|
||||||
query = Tag.joins(tag_groups: :tag_group_permissions)
|
query =
|
||||||
.where('tag_group_permissions.permission_type = ?',
|
Tag.joins(tag_groups: :tag_group_permissions).where(
|
||||||
TagGroupPermission.permission_types[:readonly])
|
"tag_group_permissions.permission_type = ?",
|
||||||
|
TagGroupPermission.permission_types[:readonly],
|
||||||
|
)
|
||||||
|
|
||||||
query.pluck(:name)
|
query.pluck(:name)
|
||||||
end
|
end
|
||||||
@ -545,14 +564,12 @@ module DiscourseTagging
|
|||||||
# explicit permissions to use these tags
|
# explicit permissions to use these tags
|
||||||
def self.permitted_tag_names(guardian = nil)
|
def self.permitted_tag_names(guardian = nil)
|
||||||
query =
|
query =
|
||||||
Tag
|
Tag.joins(tag_groups: :tag_group_permissions).where(
|
||||||
.joins(tag_groups: :tag_group_permissions)
|
tag_group_permissions: {
|
||||||
.where(
|
group_id: permitted_group_ids(guardian),
|
||||||
tag_group_permissions: {
|
permission_type: TagGroupPermission.permission_types[:full],
|
||||||
group_id: permitted_group_ids(guardian),
|
},
|
||||||
permission_type: TagGroupPermission.permission_types[:full],
|
)
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
query.pluck(:name).uniq
|
query.pluck(:name).uniq
|
||||||
end
|
end
|
||||||
@ -586,15 +603,14 @@ module DiscourseTagging
|
|||||||
tag = tag.dup
|
tag = tag.dup
|
||||||
tag.downcase! if SiteSetting.force_lowercase_tags
|
tag.downcase! if SiteSetting.force_lowercase_tags
|
||||||
tag.strip!
|
tag.strip!
|
||||||
tag.gsub!(/[[:space:]]+/, '-')
|
tag.gsub!(/[[:space:]]+/, "-")
|
||||||
tag.gsub!(/[^[:word:][:punct:]]+/, '')
|
tag.gsub!(/[^[:word:][:punct:]]+/, "")
|
||||||
tag.squeeze!('-')
|
tag.squeeze!("-")
|
||||||
tag.gsub!(TAGS_FILTER_REGEXP, '')
|
tag.gsub!(TAGS_FILTER_REGEXP, "")
|
||||||
tag[0...SiteSetting.max_tag_length]
|
tag[0...SiteSetting.max_tag_length]
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.tags_for_saving(tags_arg, guardian, opts = {})
|
def self.tags_for_saving(tags_arg, guardian, opts = {})
|
||||||
|
|
||||||
return [] unless guardian.can_tag_topics? && tags_arg.present?
|
return [] unless guardian.can_tag_topics? && tags_arg.present?
|
||||||
|
|
||||||
tag_names = Tag.where_name(tags_arg).pluck(:name)
|
tag_names = Tag.where_name(tags_arg).pluck(:name)
|
||||||
@ -609,21 +625,23 @@ module DiscourseTagging
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {})
|
def self.add_or_create_tags_by_name(taggable, tag_names_arg, opts = {})
|
||||||
tag_names = DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) || []
|
tag_names =
|
||||||
|
DiscourseTagging.tags_for_saving(tag_names_arg, Guardian.new(Discourse.system_user), opts) ||
|
||||||
|
[]
|
||||||
if taggable.tags.pluck(:name).sort != tag_names.sort
|
if taggable.tags.pluck(:name).sort != tag_names.sort
|
||||||
taggable.tags = Tag.where_name(tag_names).all
|
taggable.tags = Tag.where_name(tag_names).all
|
||||||
new_tag_names = taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : []
|
new_tag_names =
|
||||||
|
taggable.tags.size < tag_names.size ? tag_names - taggable.tags.map(&:name) : []
|
||||||
taggable.tags << Tag.where(target_tag_id: taggable.tags.map(&:id)).all
|
taggable.tags << Tag.where(target_tag_id: taggable.tags.map(&:id)).all
|
||||||
new_tag_names.each do |name|
|
new_tag_names.each { |name| taggable.tags << Tag.create(name: name) }
|
||||||
taggable.tags << Tag.create(name: name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns true if all were added successfully, or an Array of the
|
# Returns true if all were added successfully, or an Array of the
|
||||||
# tags that failed to be added, with errors on each Tag.
|
# tags that failed to be added, with errors on each Tag.
|
||||||
def self.add_or_create_synonyms_by_name(target_tag, synonym_names)
|
def self.add_or_create_synonyms_by_name(target_tag, synonym_names)
|
||||||
tag_names = DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || []
|
tag_names =
|
||||||
|
DiscourseTagging.tags_for_saving(synonym_names, Guardian.new(Discourse.system_user)) || []
|
||||||
tag_names -= [target_tag.name]
|
tag_names -= [target_tag.name]
|
||||||
existing = Tag.where_name(tag_names).all
|
existing = Tag.where_name(tag_names).all
|
||||||
target_tag.synonyms << existing
|
target_tag.synonyms << existing
|
||||||
@ -642,6 +660,6 @@ module DiscourseTagging
|
|||||||
|
|
||||||
def self.muted_tags(user)
|
def self.muted_tags(user)
|
||||||
return [] unless user
|
return [] unless user
|
||||||
TagUser.lookup(user, :muted).joins(:tag).pluck('tags.name')
|
TagUser.lookup(user, :muted).joins(:tag).pluck("tags.name")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module DiscourseUpdates
|
module DiscourseUpdates
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
|
|
||||||
def check_version
|
def check_version
|
||||||
attrs = {
|
attrs = {
|
||||||
installed_version: Discourse::VERSION::STRING,
|
installed_version: Discourse::VERSION::STRING,
|
||||||
installed_sha: (Discourse.git_version == 'unknown' ? nil : Discourse.git_version),
|
installed_sha: (Discourse.git_version == "unknown" ? nil : Discourse.git_version),
|
||||||
installed_describe: Discourse.full_version,
|
installed_describe: Discourse.full_version,
|
||||||
git_branch: Discourse.git_branch,
|
git_branch: Discourse.git_branch,
|
||||||
updated_at: updated_at,
|
updated_at: updated_at,
|
||||||
@ -17,7 +15,7 @@ module DiscourseUpdates
|
|||||||
attrs.merge!(
|
attrs.merge!(
|
||||||
latest_version: latest_version,
|
latest_version: latest_version,
|
||||||
critical_updates: critical_updates_available?,
|
critical_updates: critical_updates_available?,
|
||||||
missing_versions_count: missing_versions_count
|
missing_versions_count: missing_versions_count,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -25,19 +23,24 @@ module DiscourseUpdates
|
|||||||
|
|
||||||
# replace -commit_count with +commit_count
|
# replace -commit_count with +commit_count
|
||||||
if version_info.installed_describe =~ /-(\d+)-/
|
if version_info.installed_describe =~ /-(\d+)-/
|
||||||
version_info.installed_describe = version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}")
|
version_info.installed_describe =
|
||||||
|
version_info.installed_describe.gsub(/-(\d+)-.*/, " +#{$1}")
|
||||||
end
|
end
|
||||||
|
|
||||||
if SiteSetting.version_checks?
|
if SiteSetting.version_checks?
|
||||||
is_stale_data =
|
is_stale_data =
|
||||||
(version_info.missing_versions_count == 0 && version_info.latest_version != version_info.installed_version) ||
|
(
|
||||||
(version_info.missing_versions_count != 0 && version_info.latest_version == version_info.installed_version)
|
version_info.missing_versions_count == 0 &&
|
||||||
|
version_info.latest_version != version_info.installed_version
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
version_info.missing_versions_count != 0 &&
|
||||||
|
version_info.latest_version == version_info.installed_version
|
||||||
|
)
|
||||||
|
|
||||||
# Handle cases when version check data is old so we report something that makes sense
|
# Handle cases when version check data is old so we report something that makes sense
|
||||||
if version_info.updated_at.nil? || # never performed a version check
|
if version_info.updated_at.nil? || last_installed_version != Discourse::VERSION::STRING || # never performed a version check # upgraded since the last version check
|
||||||
last_installed_version != Discourse::VERSION::STRING || # upgraded since the last version check
|
is_stale_data
|
||||||
is_stale_data
|
|
||||||
|
|
||||||
Jobs.enqueue(:version_check, all_sites: true)
|
Jobs.enqueue(:version_check, all_sites: true)
|
||||||
version_info.version_check_pending = true
|
version_info.version_check_pending = true
|
||||||
|
|
||||||
@ -48,9 +51,8 @@ module DiscourseUpdates
|
|||||||
end
|
end
|
||||||
|
|
||||||
version_info.stale_data =
|
version_info.stale_data =
|
||||||
version_info.version_check_pending ||
|
version_info.version_check_pending || (updated_at && updated_at < 48.hours.ago) ||
|
||||||
(updated_at && updated_at < 48.hours.ago) ||
|
is_stale_data
|
||||||
is_stale_data
|
|
||||||
end
|
end
|
||||||
|
|
||||||
version_info
|
version_info
|
||||||
@ -82,7 +84,7 @@ module DiscourseUpdates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def critical_updates_available?
|
def critical_updates_available?
|
||||||
(Discourse.redis.get(critical_updates_available_key) || false) == 'true'
|
(Discourse.redis.get(critical_updates_available_key) || false) == "true"
|
||||||
end
|
end
|
||||||
|
|
||||||
def critical_updates_available=(arg)
|
def critical_updates_available=(arg)
|
||||||
@ -110,7 +112,7 @@ module DiscourseUpdates
|
|||||||
# store the list in redis
|
# store the list in redis
|
||||||
version_keys = []
|
version_keys = []
|
||||||
versions[0, 5].each do |v|
|
versions[0, 5].each do |v|
|
||||||
key = "#{missing_versions_key_prefix}:#{v['version']}"
|
key = "#{missing_versions_key_prefix}:#{v["version"]}"
|
||||||
Discourse.redis.mapped_hmset key, v
|
Discourse.redis.mapped_hmset key, v
|
||||||
version_keys << key
|
version_keys << key
|
||||||
end
|
end
|
||||||
@ -140,11 +142,21 @@ module DiscourseUpdates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def new_features
|
def new_features
|
||||||
entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
|
entries =
|
||||||
|
begin
|
||||||
|
JSON.parse(Discourse.redis.get(new_features_key))
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
return nil if entries.nil?
|
return nil if entries.nil?
|
||||||
|
|
||||||
entries.select! do |item|
|
entries.select! do |item|
|
||||||
item["discourse_version"].nil? || Discourse.has_needed_version?(current_version, item["discourse_version"]) rescue nil
|
begin
|
||||||
|
item["discourse_version"].nil? ||
|
||||||
|
Discourse.has_needed_version?(current_version, item["discourse_version"])
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
entries.sort_by { |item| Time.zone.parse(item["created_at"]).to_i }.reverse
|
entries.sort_by { |item| Time.zone.parse(item["created_at"]).to_i }.reverse
|
||||||
@ -170,7 +182,12 @@ module DiscourseUpdates
|
|||||||
end
|
end
|
||||||
|
|
||||||
def mark_new_features_as_seen(user_id)
|
def mark_new_features_as_seen(user_id)
|
||||||
entries = JSON.parse(Discourse.redis.get(new_features_key)) rescue nil
|
entries =
|
||||||
|
begin
|
||||||
|
JSON.parse(Discourse.redis.get(new_features_key))
|
||||||
|
rescue StandardError
|
||||||
|
nil
|
||||||
|
end
|
||||||
return nil if entries.nil?
|
return nil if entries.nil?
|
||||||
last_seen = entries.max_by { |x| x["created_at"] }
|
last_seen = entries.max_by { |x| x["created_at"] }
|
||||||
Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
|
Discourse.redis.set(new_features_last_seen_key(user_id), last_seen["created_at"])
|
||||||
@ -204,39 +221,39 @@ module DiscourseUpdates
|
|||||||
private
|
private
|
||||||
|
|
||||||
def last_installed_version_key
|
def last_installed_version_key
|
||||||
'last_installed_version'
|
"last_installed_version"
|
||||||
end
|
end
|
||||||
|
|
||||||
def latest_version_key
|
def latest_version_key
|
||||||
'discourse_latest_version'
|
"discourse_latest_version"
|
||||||
end
|
end
|
||||||
|
|
||||||
def critical_updates_available_key
|
def critical_updates_available_key
|
||||||
'critical_updates_available'
|
"critical_updates_available"
|
||||||
end
|
end
|
||||||
|
|
||||||
def missing_versions_count_key
|
def missing_versions_count_key
|
||||||
'missing_versions_count'
|
"missing_versions_count"
|
||||||
end
|
end
|
||||||
|
|
||||||
def updated_at_key
|
def updated_at_key
|
||||||
'last_version_check_at'
|
"last_version_check_at"
|
||||||
end
|
end
|
||||||
|
|
||||||
def missing_versions_list_key
|
def missing_versions_list_key
|
||||||
'missing_versions'
|
"missing_versions"
|
||||||
end
|
end
|
||||||
|
|
||||||
def missing_versions_key_prefix
|
def missing_versions_key_prefix
|
||||||
'missing_version'
|
"missing_version"
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_features_endpoint
|
def new_features_endpoint
|
||||||
'https://meta.discourse.org/new-features.json'
|
"https://meta.discourse.org/new-features.json"
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_features_key
|
def new_features_key
|
||||||
'new_features'
|
"new_features"
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_features_last_seen_key(user_id)
|
def new_features_last_seen_key(user_id)
|
||||||
|
@ -18,13 +18,13 @@ class DiskSpace
|
|||||||
end
|
end
|
||||||
|
|
||||||
def self.free(path)
|
def self.free(path)
|
||||||
output = Discourse::Utils.execute_command('df', '-Pk', path)
|
output = Discourse::Utils.execute_command("df", "-Pk", path)
|
||||||
size_line = output.split("\n")[1]
|
size_line = output.split("\n")[1]
|
||||||
size_line.split(/\s+/)[3].to_i * 1024
|
size_line.split(/\s+/)[3].to_i * 1024
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.percent_free(path)
|
def self.percent_free(path)
|
||||||
output = Discourse::Utils.execute_command('df', '-P', path)
|
output = Discourse::Utils.execute_command("df", "-P", path)
|
||||||
size_line = output.split("\n")[1]
|
size_line = output.split("\n")[1]
|
||||||
size_line.split(/\s+/)[4].to_i
|
size_line.split(/\s+/)[4].to_i
|
||||||
end
|
end
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'message_bus/distributed_cache'
|
require "message_bus/distributed_cache"
|
||||||
|
|
||||||
class DistributedCache < MessageBus::DistributedCache
|
class DistributedCache < MessageBus::DistributedCache
|
||||||
def initialize(key, manager: nil, namespace: true)
|
def initialize(key, manager: nil, namespace: true)
|
||||||
super(
|
super(key, manager: manager, namespace: namespace, app_version: Discourse.git_version)
|
||||||
key,
|
|
||||||
manager: manager,
|
|
||||||
namespace: namespace,
|
|
||||||
app_version: Discourse.git_version
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Defer setting of the key in the cache for performance critical path to avoid
|
# Defer setting of the key in the cache for performance critical path to avoid
|
||||||
# waiting on MessageBus to publish the message which involves writing to Redis.
|
# waiting on MessageBus to publish the message which involves writing to Redis.
|
||||||
def defer_set(k, v)
|
def defer_set(k, v)
|
||||||
Scheduler::Defer.later("#{@key}_set") do
|
Scheduler::Defer.later("#{@key}_set") { self[k] = v }
|
||||||
self[k] = v
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def defer_get_set(k, &block)
|
def defer_get_set(k, &block)
|
||||||
|
@ -31,11 +31,7 @@ class DistributedMutex
|
|||||||
LUA
|
LUA
|
||||||
|
|
||||||
def self.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk)
|
def self.synchronize(key, redis: nil, validity: DEFAULT_VALIDITY, &blk)
|
||||||
self.new(
|
self.new(key, redis: redis, validity: validity).synchronize(&blk)
|
||||||
key,
|
|
||||||
redis: redis,
|
|
||||||
validity: validity
|
|
||||||
).synchronize(&blk)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(key, redis: nil, validity: DEFAULT_VALIDITY)
|
def initialize(key, redis: nil, validity: DEFAULT_VALIDITY)
|
||||||
@ -58,7 +54,9 @@ class DistributedMutex
|
|||||||
ensure
|
ensure
|
||||||
current_time = redis.time[0]
|
current_time = redis.time[0]
|
||||||
if current_time > expire_time
|
if current_time > expire_time
|
||||||
warn("held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs")
|
warn(
|
||||||
|
"held for too long, expected max: #{@validity} secs, took an extra #{current_time - expire_time} secs",
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
unlocked = UNLOCK_SCRIPT.eval(redis, [prefixed_key], [expire_time.to_s])
|
unlocked = UNLOCK_SCRIPT.eval(redis, [prefixed_key], [expire_time.to_s])
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rate_limiter'
|
require "rate_limiter"
|
||||||
class EditRateLimiter < RateLimiter
|
class EditRateLimiter < RateLimiter
|
||||||
def initialize(user)
|
def initialize(user)
|
||||||
limit = SiteSetting.max_edits_per_day
|
limit = SiteSetting.max_edits_per_day
|
||||||
|
10
lib/email.rb
10
lib/email.rb
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'mail'
|
require "mail"
|
||||||
|
|
||||||
module Email
|
module Email
|
||||||
# See https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml#smtp-enhanced-status-codes-1
|
# See https://www.iana.org/assignments/smtp-enhanced-status-codes/smtp-enhanced-status-codes.xhtml#smtp-enhanced-status-codes-1
|
||||||
@ -21,19 +21,19 @@ module Email
|
|||||||
def self.obfuscate(email)
|
def self.obfuscate(email)
|
||||||
return email if !Email.is_valid?(email)
|
return email if !Email.is_valid?(email)
|
||||||
|
|
||||||
first, _, last = email.rpartition('@')
|
first, _, last = email.rpartition("@")
|
||||||
|
|
||||||
# Obfuscate each last part, except tld
|
# Obfuscate each last part, except tld
|
||||||
last = last.split('.')
|
last = last.split(".")
|
||||||
tld = last.pop
|
tld = last.pop
|
||||||
last.map! { |part| obfuscate_part(part) }
|
last.map! { |part| obfuscate_part(part) }
|
||||||
last << tld
|
last << tld
|
||||||
|
|
||||||
"#{obfuscate_part(first)}@#{last.join('.')}"
|
"#{obfuscate_part(first)}@#{last.join(".")}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.cleanup_alias(name)
|
def self.cleanup_alias(name)
|
||||||
name ? name.gsub(/[:<>,"]/, '') : name
|
name ? name.gsub(/[:<>,"]/, "") : name
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.extract_parts(raw)
|
def self.extract_parts(raw)
|
||||||
|
@ -2,12 +2,7 @@
|
|||||||
|
|
||||||
module Email
|
module Email
|
||||||
class AuthenticationResults
|
class AuthenticationResults
|
||||||
VERDICT = Enum.new(
|
VERDICT = Enum.new(:gray, :pass, :fail, start: 0)
|
||||||
:gray,
|
|
||||||
:pass,
|
|
||||||
:fail,
|
|
||||||
start: 0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def initialize(headers)
|
def initialize(headers)
|
||||||
@authserv_id = SiteSetting.email_in_authserv_id
|
@authserv_id = SiteSetting.email_in_authserv_id
|
||||||
@ -16,11 +11,10 @@ module Email
|
|||||||
end
|
end
|
||||||
|
|
||||||
def results
|
def results
|
||||||
@results ||= Array(@headers).map do |header|
|
@results ||=
|
||||||
parse_header(header.to_s)
|
Array(@headers)
|
||||||
end.filter do |result|
|
.map { |header| parse_header(header.to_s) }
|
||||||
@authserv_id.blank? || @authserv_id == result[:authserv_id]
|
.filter { |result| @authserv_id.blank? || @authserv_id == result[:authserv_id] }
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def action
|
def action
|
||||||
@ -55,7 +49,8 @@ module Email
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? && verdict == VERDICT[:pass]
|
verdict = VERDICT[:gray] if SiteSetting.email_in_authserv_id.blank? &&
|
||||||
|
verdict == VERDICT[:pass]
|
||||||
verdict
|
verdict
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -67,10 +62,11 @@ module Email
|
|||||||
authres_version = /\d+#{cfws}?/
|
authres_version = /\d+#{cfws}?/
|
||||||
no_result = /#{cfws}?;#{cfws}?none/
|
no_result = /#{cfws}?;#{cfws}?none/
|
||||||
keyword = /([a-zA-Z0-9-]*[a-zA-Z0-9])/
|
keyword = /([a-zA-Z0-9-]*[a-zA-Z0-9])/
|
||||||
authres_payload = /\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/
|
authres_payload =
|
||||||
|
/\A#{cfws}?#{authserv_id}(?:#{cfws}#{authres_version})?(?:#{no_result}|([\S\s]*))/
|
||||||
|
|
||||||
method_version = authres_version
|
method_version = authres_version
|
||||||
method = /#{keyword}\s*(?:#{cfws}?\/#{cfws}?#{method_version})?/
|
method = %r{#{keyword}\s*(?:#{cfws}?/#{cfws}?#{method_version})?}
|
||||||
result = keyword
|
result = keyword
|
||||||
methodspec = /#{cfws}?#{method}#{cfws}?=#{cfws}?#{result}/
|
methodspec = /#{cfws}?#{method}#{cfws}?=#{cfws}?#{result}/
|
||||||
reasonspec = /reason#{cfws}?=#{cfws}?#{value}/
|
reasonspec = /reason#{cfws}?=#{cfws}?#{value}/
|
||||||
@ -87,27 +83,21 @@ module Email
|
|||||||
|
|
||||||
if resinfo_val
|
if resinfo_val
|
||||||
resinfo_scan = resinfo_val.scan(resinfo)
|
resinfo_scan = resinfo_val.scan(resinfo)
|
||||||
parsed_resinfo = resinfo_scan.map do |x|
|
parsed_resinfo =
|
||||||
{
|
resinfo_scan.map do |x|
|
||||||
method: x[2],
|
{
|
||||||
result: x[8],
|
method: x[2],
|
||||||
reason: x[12] || x[13],
|
result: x[8],
|
||||||
props: x[-1].scan(propspec).map do |y|
|
reason: x[12] || x[13],
|
||||||
{
|
props:
|
||||||
ptype: y[0],
|
x[-1]
|
||||||
property: y[4],
|
.scan(propspec)
|
||||||
pvalue: y[8] || y[9]
|
.map { |y| { ptype: y[0], property: y[4], pvalue: y[8] || y[9] } },
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{ authserv_id: parsed_authserv_id, resinfo: parsed_resinfo }
|
||||||
authserv_id: parsed_authserv_id,
|
|
||||||
resinfo: parsed_resinfo
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -5,11 +5,11 @@ module Email
|
|||||||
def build_email(*builder_args)
|
def build_email(*builder_args)
|
||||||
builder = Email::MessageBuilder.new(*builder_args)
|
builder = Email::MessageBuilder.new(*builder_args)
|
||||||
headers(builder.header_args) if builder.header_args.present?
|
headers(builder.header_args) if builder.header_args.present?
|
||||||
mail(builder.build_args).tap { |message|
|
mail(builder.build_args).tap do |message|
|
||||||
if message && h = builder.html_part
|
if message && h = builder.html_part
|
||||||
message.html_part = h
|
message.html_part = h
|
||||||
end
|
end
|
||||||
}
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user