2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2019-12-03 17:05:53 +08:00
# rubocop:disable Style/GlobalVars
2019-05-03 06:17:27 +08:00
2013-05-31 06:41:29 +08:00
require 'cache'
2017-03-17 14:21:30 +08:00
require 'open3'
2017-12-22 05:29:11 +08:00
require_dependency 'route_format'
2013-08-23 14:21:52 +08:00
require_dependency 'plugin/instance'
2013-10-09 12:10:37 +08:00
require_dependency 'auth/default_current_user_provider'
2015-04-28 01:06:53 +08:00
require_dependency 'version'
2017-09-09 01:38:46 +08:00
require 'digest/sha1'
2013-05-31 06:41:29 +08:00
2013-02-06 03:16:51 +08:00
module Discourse
2018-10-08 15:47:38 +08:00
DB_POST_MIGRATE_PATH || = " db/post_migrate "
2021-01-29 10:14:49 +08:00
REQUESTED_HOSTNAME || = " REQUESTED_HOSTNAME "
2013-02-06 03:16:51 +08:00
2014-04-17 13:57:17 +08:00
require 'sidekiq/exception_handler'
2014-02-21 11:30:25 +08:00
class SidekiqExceptionHandler
extend Sidekiq :: ExceptionHandler
end
2017-03-17 14:21:30 +08:00
class Utils
2020-08-07 22:28:43 +08:00
URI_REGEXP || = URI . regexp ( %w{ http https } )
2020-08-06 12:25:03 +08:00
2019-11-07 23:47:16 +08:00
# Usage:
# Discourse::Utils.execute_command("pwd", chdir: 'mydirectory')
# or with a block
# Discourse::Utils.execute_command(chdir: 'mydirectory') do |runner|
# runner.exec("pwd")
# end
def self . execute_command ( * command , ** args )
runner = CommandRunner . new ( ** args )
2017-03-17 14:21:30 +08:00
2019-11-07 23:47:16 +08:00
if block_given?
raise RuntimeError . new ( " Cannot pass command and block to execute_command " ) if command . present?
yield runner
else
runner . exec ( * command )
2017-03-17 14:21:30 +08:00
end
end
def self . pretty_logs ( logs )
2020-04-30 14:48:34 +08:00
logs . join ( " \n " )
2017-03-17 14:21:30 +08:00
end
2019-11-07 23:47:16 +08:00
2021-08-04 01:06:50 +08:00
def self . logs_markdown ( logs , user : , filename : 'log.txt' )
# Reserve 250 characters for the rest of the text
max_logs_length = SiteSetting . max_post_length - 250
pretty_logs = Discourse :: Utils . pretty_logs ( logs )
# If logs are short, try to inline them
if pretty_logs . size < max_logs_length
return << ~ TEXT
` ` ` text
#{pretty_logs}
` ` `
TEXT
end
# Try to create an upload for the logs
upload = Dir . mktmpdir do | dir |
File . write ( File . join ( dir , filename ) , pretty_logs )
zipfile = Compression :: Zip . new . compress ( dir , filename )
File . open ( zipfile ) do | file |
UploadCreator . new (
file ,
File . basename ( zipfile ) ,
type : 'backup_logs' ,
for_export : 'true'
) . create_for ( user . id )
end
end
if upload . persisted?
return UploadMarkdown . new ( upload ) . attachment_markdown
else
Rails . logger . warn ( " Failed to upload the backup logs file: #{ upload . errors . full_messages } " )
end
# If logs are long and upload cannot be created, show trimmed logs
<< ~ TEXT
` ` ` text
...
#{pretty_logs.last(max_logs_length)}
` ` `
TEXT
end
2020-03-05 01:28:26 +08:00
def self . atomic_write_file ( destination , contents )
begin
return if File . read ( destination ) == contents
rescue Errno :: ENOENT
end
FileUtils . mkdir_p ( File . join ( Rails . root , 'tmp' ) )
temp_destination = File . join ( Rails . root , 'tmp' , SecureRandom . hex )
File . open ( temp_destination , " w " ) do | fd |
fd . write ( contents )
fd . fsync ( )
end
2021-08-09 18:20:26 +08:00
FileUtils . mv ( temp_destination , destination )
2020-03-05 01:28:26 +08:00
nil
end
def self . atomic_ln_s ( source , destination )
begin
return if File . readlink ( destination ) == source
rescue Errno :: ENOENT , Errno :: EINVAL
end
FileUtils . mkdir_p ( File . join ( Rails . root , 'tmp' ) )
temp_destination = File . join ( Rails . root , 'tmp' , SecureRandom . hex )
execute_command ( 'ln' , '-s' , source , temp_destination )
2021-08-09 18:20:26 +08:00
FileUtils . mv ( temp_destination , destination )
2020-03-05 01:28:26 +08:00
nil
end
2019-11-07 23:47:16 +08:00
private
class CommandRunner
def initialize ( ** init_params )
@init_params = init_params
end
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?
execute_command ( * command , ** @init_params . merge ( exec_params ) )
end
private
2021-04-15 23:29:37 +08:00
def execute_command ( * command , timeout : nil , failure_message : " " , success_status_codes : [ 0 ] , chdir : " . " , unsafe_shell : false )
2021-04-12 20:53:41 +08:00
env = nil
env = command . shift if command [ 0 ] . is_a? ( Hash )
2021-04-15 23:29:37 +08:00
if ! unsafe_shell && ( command . length == 1 ) && command [ 0 ] . include? ( " " )
# Sending a single string to Process.spawn will launch a shell
# This means various things (e.g. subshells) are possible, and could present injection risk
raise " Arguments should be provided as separate strings "
end
2021-04-12 11:55:54 +08:00
if timeout
# will send a TERM after timeout
# will send a KILL after timeout * 2
command = [ " timeout " , " -k " , " #{ timeout . to_f * 2 } " , timeout . to_s ] + command
end
2021-04-12 20:53:41 +08:00
args = command
args = [ env ] + command if env
stdout , stderr , status = Open3 . capture3 ( * args , chdir : chdir )
2019-11-07 23:47:16 +08:00
if ! status . exited? || ! success_status_codes . include? ( status . exitstatus )
failure_message = " #{ failure_message } \n " if ! failure_message . blank?
raise " #{ caller [ 0 ] } : #{ failure_message } #{ stderr } "
end
stdout
end
end
2017-03-17 14:21:30 +08:00
end
2014-07-18 04:22:46 +08:00
# Log an exception.
#
2014-07-18 06:07:25 +08:00
# If your code is in a scheduled job, it is recommended to use the
# error_context() method in Jobs::Base to pass the job arguments and any
# other desired context.
2014-07-18 04:22:46 +08:00
# See app/jobs/base.rb for the error_context function.
2015-02-10 04:47:46 +08:00
def self . handle_job_exception ( ex , context = { } , parent_logger = nil )
2018-08-01 05:12:55 +08:00
return if ex . class == Jobs :: HandledExceptionWrapper
2014-02-21 11:30:25 +08:00
context || = { }
parent_logger || = SidekiqExceptionHandler
cm = RailsMultisite :: ConnectionManagement
parent_logger . handle_exception ( ex , {
current_db : cm . current_db ,
current_hostname : cm . current_hostname
} . merge ( context ) )
2019-04-08 22:57:47 +08:00
raise ex if Rails . env . test?
2014-02-21 11:30:25 +08:00
end
2013-06-19 08:31:19 +08:00
# Expected less matches than what we got in a find
2015-03-23 09:16:21 +08:00
class TooManyMatches < StandardError ; end
2013-06-19 08:31:19 +08:00
2013-02-26 00:42:20 +08:00
# When they try to do something they should be logged in for
2015-03-23 09:16:21 +08:00
class NotLoggedIn < StandardError ; end
2013-02-06 03:16:51 +08:00
# When the input is somehow bad
2015-03-23 09:16:21 +08:00
class InvalidParameters < StandardError ; end
2013-02-06 03:16:51 +08:00
# When they don't have permission to do something
2015-09-18 15:14:10 +08:00
class InvalidAccess < StandardError
2019-10-08 19:15:08 +08:00
attr_reader :obj
attr_reader :opts
attr_reader :custom_message
2020-11-24 19:06:52 +08:00
attr_reader :custom_message_params
2019-10-08 19:15:08 +08:00
attr_reader :group
2017-09-23 22:39:58 +08:00
def initialize ( msg = nil , obj = nil , opts = nil )
2015-09-18 15:14:10 +08:00
super ( msg )
2017-09-23 22:39:58 +08:00
2018-02-10 08:09:54 +08:00
@opts = opts || { }
2015-09-18 15:14:10 +08:00
@obj = obj
2019-10-08 19:15:08 +08:00
@custom_message = opts [ :custom_message ] if @opts [ :custom_message ]
2020-11-24 19:06:52 +08:00
@custom_message_params = opts [ :custom_message_params ] if @opts [ :custom_message_params ]
2019-10-08 19:15:08 +08:00
@group = opts [ :group ] if @opts [ :group ]
2015-09-18 15:14:10 +08:00
end
end
2013-02-06 03:16:51 +08:00
# When something they want is not found
2018-08-09 13:05:12 +08:00
class NotFound < StandardError
attr_reader :status
attr_reader :check_permalinks
attr_reader :original_path
2019-10-08 19:15:08 +08:00
attr_reader :custom_message
def initialize ( msg = nil , status : 404 , check_permalinks : false , original_path : nil , custom_message : nil )
super ( msg )
2018-08-09 13:05:12 +08:00
@status = status
@check_permalinks = check_permalinks
@original_path = original_path
2019-10-08 19:15:08 +08:00
@custom_message = custom_message
2018-08-09 13:05:12 +08:00
end
end
2013-02-06 03:16:51 +08:00
2013-06-05 06:34:53 +08:00
# When a setting is missing
2015-03-23 09:16:21 +08:00
class SiteSettingMissing < StandardError ; end
2013-06-05 06:34:53 +08:00
2013-11-06 02:04:47 +08:00
# When ImageMagick is missing
2015-03-23 09:16:21 +08:00
class ImageMagickMissing < StandardError ; end
2013-11-06 02:04:47 +08:00
2014-02-13 12:37:28 +08:00
# When read-only mode is enabled
2015-03-23 09:16:21 +08:00
class ReadOnly < StandardError ; end
2014-02-13 12:37:28 +08:00
2013-07-29 13:13:13 +08:00
# Cross site request forgery
2015-03-23 09:16:21 +08:00
class CSRF < StandardError ; end
2013-07-29 13:13:13 +08:00
2017-08-07 09:43:09 +08:00
class Deprecation < StandardError ; end
2019-02-07 22:27:42 +08:00
class ScssError < StandardError ; end
2013-12-24 07:50:36 +08:00
def self . filters
2021-08-10 22:30:34 +08:00
@filters || = [ :latest , :unread , :new , :unseen , :top , :read , :posted , :bookmarks ]
2013-12-24 07:50:36 +08:00
end
def self . anonymous_filters
2015-07-27 14:46:50 +08:00
@anonymous_filters || = [ :latest , :top , :categories ]
2013-12-24 07:50:36 +08:00
end
def self . top_menu_items
2020-07-22 21:56:36 +08:00
@top_menu_items || = Discourse . filters + [ :categories ]
2013-12-24 07:50:36 +08:00
end
def self . anonymous_top_menu_items
2019-11-11 21:18:10 +08:00
@anonymous_top_menu_items || = Discourse . anonymous_filters + [ :categories , :top ]
2013-12-24 07:50:36 +08:00
end
2016-04-06 16:57:59 +08:00
PIXEL_RATIOS || = [ 1 , 1 . 5 , 2 , 3 ]
2015-05-29 15:57:54 +08:00
2015-05-25 23:59:00 +08:00
def self . avatar_sizes
2015-05-29 15:57:54 +08:00
# TODO: should cache these when we get a notification system for site settings
set = Set . new
SiteSetting . avatar_sizes . split ( " | " ) . map ( & :to_i ) . each do | size |
PIXEL_RATIOS . each do | pixel_ratio |
2019-08-27 23:03:20 +08:00
set << ( size * pixel_ratio ) . to_i
2015-05-29 15:57:54 +08:00
end
end
2015-05-26 13:41:50 +08:00
set
2015-05-25 23:59:00 +08:00
end
2013-08-01 13:59:57 +08:00
def self . activate_plugins!
2015-04-28 01:06:53 +08:00
@plugins = [ ]
2019-11-19 07:15:09 +08:00
Plugin :: Instance . find_all ( " #{ Rails . root } /plugins " ) . each do | p |
2015-04-28 01:06:53 +08:00
v = p . metadata . required_version || Discourse :: VERSION :: STRING
if Discourse . has_needed_version? ( Discourse :: VERSION :: STRING , v )
p . activate!
@plugins << p
else
STDERR . puts " Could not activate #{ p . metadata . name } , discourse does not meet required version ( #{ v } ) "
end
end
2020-01-11 04:06:15 +08:00
DiscourseEvent . trigger ( :after_plugin_activation )
2013-08-01 13:59:57 +08:00
end
2015-02-05 05:23:39 +08:00
def self . disabled_plugin_names
2016-06-30 22:55:01 +08:00
plugins . select { | p | ! p . enabled? } . map ( & :name )
2015-02-05 05:23:39 +08:00
end
2013-08-01 13:59:57 +08:00
def self . plugins
2015-02-11 00:18:16 +08:00
@plugins || = [ ]
2013-08-01 13:59:57 +08:00
end
2018-05-08 13:24:58 +08:00
def self . hidden_plugins
@hidden_plugins || = [ ]
end
2018-05-09 07:52:21 +08:00
def self . visible_plugins
2018-05-08 13:24:58 +08:00
self . plugins - self . hidden_plugins
end
2017-01-13 04:43:09 +08:00
def self . plugin_themes
@plugin_themes || = plugins . map ( & :themes ) . flatten
end
2016-11-15 08:42:55 +08:00
def self . official_plugins
plugins . find_all { | p | p . metadata . official? }
end
def self . unofficial_plugins
plugins . find_all { | p | ! p . metadata . official? }
end
2019-07-15 22:52:54 +08:00
def self . find_plugins ( args )
plugins . select do | plugin |
next if args [ :include_official ] == false && plugin . metadata . official?
next if args [ :include_unofficial ] == false && ! plugin . metadata . official?
2019-11-01 17:50:31 +08:00
next if ! args [ :include_disabled ] && ! plugin . enabled?
2019-07-15 22:52:54 +08:00
true
end
end
2021-04-23 22:24:42 +08:00
def self . apply_asset_filters ( plugins , type , request )
filter_opts = asset_filter_options ( type , request )
plugins . select do | plugin |
plugin . asset_filters . all? { | b | b . call ( type , request , filter_opts ) }
2020-03-13 23:30:31 +08:00
end
2021-04-23 22:24:42 +08:00
end
def self . asset_filter_options ( type , request )
result = { }
return result if request . blank?
path = request . fullpath
result [ :path ] = path if path . present?
# When we bootstrap using the JSON method, we want to be able to filter assets on
# the path we're bootstrapping for.
asset_path = request . headers [ " HTTP_X_DISCOURSE_ASSET_PATH " ]
result [ :path ] = asset_path if asset_path . present?
result
end
def self . find_plugin_css_assets ( args )
plugins = apply_asset_filters ( self . find_plugins ( args ) , :css , args [ :request ] )
2020-03-13 23:30:31 +08:00
2019-09-16 21:56:19 +08:00
assets = [ ]
targets = [ nil ]
targets << :mobile if args [ :mobile_view ]
targets << :desktop if args [ :desktop_view ]
targets . each do | target |
assets += plugins . find_all do | plugin |
plugin . css_asset_exists? ( target )
end . map do | plugin |
target . nil? ? plugin . directory_name : " #{ plugin . directory_name } _ #{ target } "
end
end
2019-08-22 11:09:10 +08:00
assets
2019-08-21 00:39:52 +08:00
end
2019-07-15 22:52:54 +08:00
def self . find_plugin_js_assets ( args )
2020-03-13 23:30:31 +08:00
plugins = self . find_plugins ( args ) . select do | plugin |
2019-07-15 22:52:54 +08:00
plugin . js_asset_exists?
2020-03-13 23:30:31 +08:00
end
2021-04-23 22:24:42 +08:00
plugins = apply_asset_filters ( plugins , :js , args [ :request ] )
2020-03-13 23:30:31 +08:00
plugins . map { | plugin | " plugins/ #{ plugin . directory_name } " }
2019-07-15 22:52:54 +08:00
end
2014-01-15 09:07:42 +08:00
def self . assets_digest
@assets_digest || = begin
digest = Digest :: MD5 . hexdigest ( ActionView :: Base . assets_manifest . assets . values . sort . join )
channel = " /global/asset-version "
2015-05-04 10:21:00 +08:00
message = MessageBus . last_message ( channel )
2014-01-15 09:07:42 +08:00
unless message && message . data == digest
2015-05-04 10:21:00 +08:00
MessageBus . publish channel , digest
2014-01-15 09:07:42 +08:00
end
digest
end
end
2018-08-09 23:29:02 +08:00
BUILTIN_AUTH || = [
2019-03-27 21:25:04 +08:00
Auth :: AuthProvider . new ( 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 :: TwitterAuthenticator . new , icon : " fab-twitter " ) ,
2019-10-08 19:10:43 +08:00
Auth :: AuthProvider . new ( authenticator : Auth :: DiscordAuthenticator . new , icon : " fab-discord " )
2018-07-31 23:18:50 +08:00
]
def self . auth_providers
BUILTIN_AUTH + DiscoursePluginRegistry . auth_providers . to_a
end
def self . enabled_auth_providers
auth_providers . select { | provider | provider . authenticator . enabled? }
end
2013-08-26 09:04:16 +08:00
def self . authenticators
# NOTE: this bypasses the site settings and gives a list of everything, we need to register every middleware
# for the cases of multisite
2018-07-31 23:18:50 +08:00
auth_providers . map ( & :authenticator )
2013-08-26 09:04:16 +08:00
end
2018-07-23 23:51:57 +08:00
def self . enabled_authenticators
authenticators . select { | authenticator | authenticator . enabled? }
2013-08-01 13:59:57 +08:00
end
2013-05-31 06:41:29 +08:00
def self . cache
2019-06-13 10:58:27 +08:00
@cache || = begin
if GlobalSetting . skip_redis?
ActiveSupport :: Cache :: MemoryStore . new
else
Cache . new
end
end
2013-05-31 06:41:29 +08:00
end
2013-02-06 03:16:51 +08:00
2020-02-18 12:11:30 +08:00
# hostname of the server, operating system level
# called os_hostname so we do no confuse it with current_hostname
def self . os_hostname
@os_hostname || =
begin
require 'socket'
Socket . gethostname
rescue = > e
warn_exception ( e , message : 'Socket.gethostname is not working' )
begin
` hostname ` . strip
rescue = > e
warn_exception ( e , message : 'hostname command is not working' )
'unknown_host'
end
end
end
2013-02-06 03:16:51 +08:00
# Get the current base URL for the current site
def self . current_hostname
2016-06-30 22:55:01 +08:00
SiteSetting . force_hostname . presence || RailsMultisite :: ConnectionManagement . current_hostname
2013-05-31 06:41:29 +08:00
end
2020-10-09 19:51:24 +08:00
def self . base_path ( default_value = " " )
2016-06-30 22:55:01 +08:00
ActionController :: Base . config . relative_url_root . presence || default_value
2013-03-14 20:01:52 +08:00
end
2020-10-09 19:51:24 +08:00
def self . base_uri ( default_value = " " )
deprecate ( " Discourse.base_uri is deprecated, use Discourse.base_path instead " )
base_path ( default_value )
end
2016-07-29 01:54:17 +08:00
def self . base_protocol
SiteSetting . force_https? ? " https " : " http "
end
2013-05-31 06:41:29 +08:00
def self . base_url_no_prefix
2016-07-29 01:54:17 +08:00
default_port = SiteSetting . force_https? ? 443 : 80
2019-05-03 06:17:27 +08:00
url = + " #{ base_protocol } :// #{ current_hostname } "
2016-06-30 22:55:01 +08:00
url << " : #{ SiteSetting . port } " if SiteSetting . port . to_i > 0 && SiteSetting . port . to_i != default_port
2019-05-06 13:26:57 +08:00
if Rails . env . development? && SiteSetting . port . blank?
url << " : #{ ENV [ " UNICORN_PORT " ] || 3000 } "
end
2016-06-30 22:55:01 +08:00
url
2013-04-05 18:38:20 +08:00
end
2013-05-31 06:41:29 +08:00
def self . base_url
2020-10-09 19:51:24 +08:00
base_url_no_prefix + base_path
2013-05-31 06:41:29 +08:00
end
2017-07-20 03:08:54 +08:00
def self . route_for ( uri )
2018-03-28 16:20:08 +08:00
unless uri . is_a? ( URI )
uri = begin
URI ( uri )
2020-11-20 17:28:14 +08:00
rescue ArgumentError , URI :: Error
2018-03-28 16:20:08 +08:00
end
end
2017-07-20 03:08:54 +08:00
return unless uri
2019-05-03 06:17:27 +08:00
path = + ( uri . path || " " )
2020-10-09 19:51:24 +08:00
if ! uri . host || ( uri . host == Discourse . current_hostname && path . start_with? ( Discourse . base_path ) )
path . slice! ( Discourse . base_path )
2017-07-20 03:08:54 +08:00
return Rails . application . routes . recognize_path ( path )
end
2017-07-21 04:01:16 +08:00
nil
rescue ActionController :: RoutingError
2017-07-20 03:08:54 +08:00
nil
end
2018-11-06 21:17:13 +08:00
class << self
alias_method :base_url_no_path , :base_url_no_prefix
end
2020-06-11 13:45:46 +08:00
READONLY_MODE_KEY_TTL || = 60
READONLY_MODE_KEY || = 'readonly_mode'
PG_READONLY_MODE_KEY || = 'readonly_mode:postgres'
2020-07-14 16:15:58 +08:00
PG_READONLY_MODE_KEY_TTL || = 300
2020-06-11 13:45:46 +08:00
USER_READONLY_MODE_KEY || = 'readonly_mode:user'
PG_FORCE_READONLY_MODE_KEY || = 'readonly_mode:postgres_force'
2016-06-29 14:19:18 +08:00
2017-01-11 18:03:36 +08:00
READONLY_KEYS || = [
2017-01-11 16:38:07 +08:00
READONLY_MODE_KEY ,
PG_READONLY_MODE_KEY ,
2020-06-11 13:45:46 +08:00
USER_READONLY_MODE_KEY ,
PG_FORCE_READONLY_MODE_KEY
2017-01-11 16:38:07 +08:00
]
def self . enable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 18:27:24 +08:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . pause! ( " pg_failover " ) if ! Sidekiq . paused?
end
2020-06-11 13:45:46 +08:00
if key == USER_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
2019-12-03 17:05:53 +08:00
Discourse . redis . set ( key , 1 )
2016-06-29 14:19:18 +08:00
else
2020-07-14 16:15:58 +08:00
ttl =
case key
when PG_READONLY_MODE_KEY
PG_READONLY_MODE_KEY_TTL
else
READONLY_MODE_KEY_TTL
end
Discourse . redis . setex ( key , ttl , 1 )
keep_readonly_mode ( key , ttl : ttl ) if ! Rails . env . test?
2016-06-29 14:19:18 +08:00
end
2016-06-29 13:55:17 +08:00
2015-05-04 10:21:00 +08:00
MessageBus . publish ( readonly_channel , true )
2013-02-06 03:16:51 +08:00
true
end
2020-07-14 16:15:58 +08:00
def self . keep_readonly_mode ( key , ttl : )
# extend the expiry by ttl minute every ttl/2 seconds
2019-02-20 10:01:18 +08:00
@mutex || = Mutex . new
@mutex . synchronize do
2018-06-19 15:44:08 +08:00
@dbs || = Set . new
@dbs << RailsMultisite :: ConnectionManagement . current_db
2018-06-19 10:15:29 +08:00
@threads || = { }
2018-06-19 15:44:08 +08:00
unless @threads [ key ] & . alive?
2018-06-19 10:15:29 +08:00
@threads [ key ] = Thread . new do
2019-02-20 10:01:18 +08:00
while @dbs . size > 0 do
2020-07-14 16:15:58 +08:00
sleep ttl / 2
2018-06-21 17:52:42 +08:00
2019-02-20 10:01:18 +08:00
@mutex . synchronize do
@dbs . each do | db |
RailsMultisite :: ConnectionManagement . with_connection ( db ) do
2020-07-14 16:15:58 +08:00
if ! Discourse . redis . expire ( key , ttl )
2019-02-20 10:01:18 +08:00
@dbs . delete ( db )
end
2018-06-19 15:44:08 +08:00
end
end
end
2018-06-19 10:15:29 +08:00
end
2016-11-10 23:44:51 +08:00
end
2015-02-12 04:50:17 +08:00
end
end
end
2017-01-11 16:38:07 +08:00
def self . disable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 18:27:24 +08:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . unpause! if Sidekiq . paused?
end
2019-12-03 17:05:53 +08:00
Discourse . redis . del ( key )
2015-05-04 10:21:00 +08:00
MessageBus . publish ( readonly_channel , false )
2013-02-06 03:16:51 +08:00
true
end
2020-06-11 13:45:46 +08:00
def self . enable_pg_force_readonly_mode
RailsMultisite :: ConnectionManagement . each_connection do
enable_readonly_mode ( PG_FORCE_READONLY_MODE_KEY )
end
true
end
def self . disable_pg_force_readonly_mode
RailsMultisite :: ConnectionManagement . each_connection do
disable_readonly_mode ( PG_FORCE_READONLY_MODE_KEY )
end
true
end
2018-06-12 00:21:29 +08:00
def self . readonly_mode? ( keys = READONLY_KEYS )
2020-06-15 09:57:44 +08:00
recently_readonly? || Discourse . redis . exists? ( * keys )
2017-01-11 16:38:07 +08:00
end
2019-01-21 13:29:29 +08:00
def self . pg_readonly_mode?
2019-12-03 17:05:53 +08:00
Discourse . redis . get ( PG_READONLY_MODE_KEY ) . present?
2019-01-21 13:29:29 +08:00
end
2019-06-21 22:08:57 +08:00
# Shared between processes
def self . postgres_last_read_only
@postgres_last_read_only || = DistributedCache . new ( 'postgres_last_read_only' , namespace : false )
end
# Per-process
def self . redis_last_read_only
@redis_last_read_only || = { }
2017-01-11 16:38:07 +08:00
end
def self . recently_readonly?
2019-12-03 17:05:53 +08:00
postgres_read_only = postgres_last_read_only [ Discourse . redis . namespace ]
redis_read_only = redis_last_read_only [ Discourse . redis . namespace ]
2019-06-21 22:08:57 +08:00
( redis_read_only . present? && redis_read_only > 15 . seconds . ago ) ||
( postgres_read_only . present? && postgres_read_only > 15 . seconds . ago )
2017-01-11 16:38:07 +08:00
end
2019-06-21 22:08:57 +08:00
def self . received_postgres_readonly!
2019-12-03 17:05:53 +08:00
postgres_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2019-06-21 22:08:57 +08:00
end
2020-06-09 16:36:04 +08:00
def self . clear_postgres_readonly!
postgres_last_read_only [ Discourse . redis . namespace ] = nil
end
2019-06-21 22:08:57 +08:00
def self . received_redis_readonly!
2019-12-03 17:05:53 +08:00
redis_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2017-01-11 16:38:07 +08:00
end
2020-06-09 16:36:04 +08:00
def self . clear_redis_readonly!
redis_last_read_only [ Discourse . redis . namespace ] = nil
end
2017-01-11 16:38:07 +08:00
def self . clear_readonly!
2020-06-09 16:36:04 +08:00
clear_redis_readonly!
clear_postgres_readonly!
2019-01-22 09:51:45 +08:00
Site . clear_anon_cache!
true
2013-02-06 03:16:51 +08:00
end
2017-08-16 10:38:30 +08:00
def self . request_refresh! ( user_ids : nil )
2014-02-21 13:52:11 +08:00
# Causes refresh on next click for all clients
#
2015-05-04 10:21:00 +08:00
# This is better than `MessageBus.publish "/file-change", ["refresh"]` because
2014-02-21 13:52:11 +08:00
# it spreads the refreshes out over a time period
2017-08-16 10:38:30 +08:00
if user_ids
2017-08-16 12:06:47 +08:00
MessageBus . publish ( " /refresh_client " , 'clobber' , user_ids : user_ids )
2017-08-16 10:38:30 +08:00
else
MessageBus . publish ( '/global/asset-version' , 'clobber' )
end
2014-02-21 13:52:11 +08:00
end
2017-10-04 11:22:23 +08:00
def self . ensure_version_file_loaded
unless @version_file_loaded
version_file = " #{ Rails . root } /config/version.rb "
require version_file if File . exists? ( version_file )
@version_file_loaded = true
end
end
2013-08-03 05:25:57 +08:00
2017-10-04 11:22:23 +08:00
def self . git_version
ensure_version_file_loaded
$git_version || =
begin
git_cmd = 'git rev-parse HEAD'
self . try_git ( git_cmd , Discourse :: VERSION :: STRING )
2019-12-03 17:05:53 +08:00
end # rubocop:disable Style/GlobalVars
2013-02-18 14:39:54 +08:00
end
2014-09-10 05:04:10 +08:00
def self . git_branch
2017-10-04 11:22:23 +08:00
ensure_version_file_loaded
$git_branch || =
begin
git_cmd = 'git rev-parse --abbrev-ref HEAD'
self . try_git ( git_cmd , 'unknown' )
end
2017-08-29 00:24:56 +08:00
end
def self . full_version
2017-10-04 11:22:23 +08:00
ensure_version_file_loaded
$full_version || =
begin
git_cmd = 'git describe --dirty --match "v[0-9]*"'
self . try_git ( git_cmd , 'unknown' )
end
2017-08-29 00:24:56 +08:00
end
2019-05-17 13:42:45 +08:00
def self . last_commit_date
ensure_version_file_loaded
$last_commit_date || =
begin
git_cmd = 'git log -1 --format="%ct"'
seconds = self . try_git ( git_cmd , nil )
seconds . nil? ? nil : DateTime . strptime ( seconds , '%s' )
end
end
2017-10-04 11:22:23 +08:00
def self . try_git ( git_cmd , default_value )
2017-08-29 00:24:56 +08:00
version_value = false
2014-09-10 05:04:10 +08:00
2017-10-04 11:22:23 +08:00
begin
version_value = ` #{ git_cmd } ` . strip
rescue
version_value = default_value
2014-09-10 05:04:10 +08:00
end
2017-08-29 00:24:56 +08:00
if version_value . empty?
version_value = default_value
end
version_value
2014-09-10 05:04:10 +08:00
end
2013-09-06 15:28:37 +08:00
# Either returns the site_contact_username user or the first admin.
def self . site_contact_user
2014-05-06 21:41:59 +08:00
user = User . find_by ( username_lower : SiteSetting . site_contact_username . downcase ) if SiteSetting . site_contact_username . present?
2015-11-25 03:37:33 +08:00
user || = ( system_user || User . admins . real . order ( :id ) . first )
2013-05-31 06:41:29 +08:00
end
2013-02-06 03:16:51 +08:00
2015-05-07 07:00:13 +08:00
SYSTEM_USER_ID || = - 1
2014-06-25 08:45:20 +08:00
2013-09-06 15:28:37 +08:00
def self . system_user
2019-10-31 23:16:26 +08:00
@system_users || = { }
current_db = RailsMultisite :: ConnectionManagement . current_db
@system_users [ current_db ] || = User . find_by ( id : SYSTEM_USER_ID )
2013-09-06 15:28:37 +08:00
end
2013-08-01 05:26:34 +08:00
def self . store
2017-10-06 13:20:01 +08:00
if SiteSetting . Upload . enable_s3_uploads
2013-08-01 05:26:34 +08:00
@s3_store_loaded || = require 'file_store/s3_store'
2013-11-06 02:04:47 +08:00
FileStore :: S3Store . new
2013-08-01 05:26:34 +08:00
else
@local_store_loaded || = require 'file_store/local_store'
2013-11-06 02:04:47 +08:00
FileStore :: LocalStore . new
2013-08-01 05:26:34 +08:00
end
end
2019-04-17 15:15:04 +08:00
def self . stats
2019-05-02 02:04:18 +08:00
PluginStore . new ( " stats " )
2019-04-17 15:15:04 +08:00
end
2013-10-09 12:10:37 +08:00
def self . current_user_provider
@current_user_provider || Auth :: DefaultCurrentUserProvider
end
def self . current_user_provider = ( val )
@current_user_provider = val
end
2013-11-06 02:04:47 +08:00
def self . asset_host
Rails . configuration . action_controller . asset_host
end
2014-02-13 12:37:28 +08:00
def self . readonly_channel
2014-02-20 01:21:41 +08:00
" /site/read-only "
2013-02-06 03:16:51 +08:00
end
2014-02-13 12:37:28 +08:00
2014-03-28 10:48:14 +08:00
# all forking servers must call this
# after fork, otherwise Discourse will be
# in a bad state
def self . after_fork
2018-06-14 16:22:02 +08:00
# note: some of this reconnecting may no longer be needed per https://github.com/redis/redis-rb/pull/414
2015-05-04 10:21:00 +08:00
MessageBus . after_fork
2014-03-28 10:48:14 +08:00
SiteSetting . after_fork
2020-06-01 10:55:53 +08:00
Discourse . redis . reconnect
2014-03-28 10:48:14 +08:00
Rails . cache . reconnect
2019-11-27 09:35:14 +08:00
Discourse . cache . reconnect
2014-05-08 06:05:28 +08:00
Logster . store . redis . reconnect
2014-04-23 09:01:17 +08:00
# shuts down all connections in the pool
2020-06-11 14:09:19 +08:00
Sidekiq . redis_pool . shutdown { | conn | conn . disconnect! }
2014-04-23 09:01:17 +08:00
# re-establish
Sidekiq . redis = sidekiq_redis_config
2016-07-16 13:11:34 +08:00
# in case v8 was initialized we want to make sure it is nil
PrettyText . reset_context
2016-11-02 10:34:20 +08:00
2020-03-11 21:43:55 +08:00
DiscourseJsProcessor :: Transpiler . reset_context if defined? DiscourseJsProcessor :: Transpiler
2016-11-02 10:34:20 +08:00
JsLocaleHelper . reset_context if defined? JsLocaleHelper
2021-06-03 14:41:16 +08:00
# 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
# working due to fork
begin
# Skip warmup in development mode - it makes boot take ~2s longer
PrettyText . cook ( " warm up **pretty text** " ) if ! Rails . env . development?
rescue = > e
Rails . logger . error ( " Failed to warm up pretty text: #{ e } " )
end
2014-05-08 06:05:28 +08:00
nil
2014-04-23 09:01:17 +08:00
end
2018-08-13 11:14:34 +08:00
# you can use Discourse.warn when you want to report custom environment
# with the error, this helps with grouping
def self . warn ( message , env = nil )
append = env ? ( + " " ) << env . map { | k , v | " #{ k } : #{ v } " } . join ( " " ) : " "
if ! ( Logster :: Logger === Rails . logger )
Rails . logger . warn ( " #{ message } #{ append } " )
return
end
loggers = [ Rails . logger ]
if Rails . logger . chained
loggers . concat ( Rails . logger . chained )
end
2018-08-13 14:33:06 +08:00
logster_env = env
2018-08-13 11:14:34 +08:00
if old_env = Thread . current [ Logster :: Logger :: LOGSTER_ENV ]
2018-08-13 14:33:06 +08:00
logster_env = Logster :: Message . populate_from_env ( old_env )
# a bit awkward by try to keep the new params
env . each do | k , v |
logster_env [ k ] = v
end
2018-08-13 11:14:34 +08:00
end
loggers . each do | logger |
if ! ( Logster :: Logger === logger )
logger . warn ( " #{ message } #{ append } " )
next
end
logger . store . report (
:: Logger :: Severity :: WARN ,
" discourse " ,
message ,
2018-08-13 14:33:06 +08:00
env : logster_env
2018-08-13 11:14:34 +08:00
)
end
2018-08-13 14:33:06 +08:00
if old_env
env . each do | k , v |
# do not leak state
logster_env . delete ( k )
end
end
nil
2018-08-13 11:14:34 +08:00
end
2017-12-01 13:23:21 +08:00
# report a warning maintaining backtrack for logster
def self . warn_exception ( e , message : " " , env : nil )
if Rails . logger . respond_to? :add_with_opts
2018-01-05 06:54:28 +08:00
env || = { }
env [ :current_db ] || = RailsMultisite :: ConnectionManagement . current_db
2017-12-01 13:23:21 +08:00
# logster
Rails . logger . add_with_opts (
:: Logger :: Severity :: WARN ,
" #{ message } : #{ e } " ,
" discourse-exception " ,
backtrace : e . backtrace . join ( " \n " ) ,
env : env
)
else
# no logster ... fallback
2020-06-11 10:49:46 +08:00
Rails . logger . warn ( " #{ message } #{ e } \n #{ e . backtrace . join ( " \n " ) } " )
2017-12-01 13:23:21 +08:00
end
rescue
STDERR . puts " Failed to report exception #{ e } #{ message } "
end
2019-01-04 01:03:01 +08:00
def self . deprecate ( warning , drop_from : nil , since : nil , raise_error : false , output_in_test : false )
2018-12-06 19:38:01 +08:00
location = caller_locations [ 1 ] . yield_self { | l | " #{ l . path } : #{ l . lineno } :in \ ` #{ l . label } \ ` " }
warning = [ " Deprecation notice: " , warning ]
warning << " (deprecated since Discourse #{ since } ) " if since
warning << " (removal in Discourse #{ drop_from } ) " if drop_from
warning << " \n At #{ location } "
warning = warning . join ( " " )
if raise_error
raise Deprecation . new ( warning )
end
2018-06-20 15:50:11 +08:00
if Rails . env == " development "
STDERR . puts ( warning )
end
2019-01-04 01:03:01 +08:00
if output_in_test && Rails . env == " test "
STDERR . puts ( warning )
end
2018-06-20 15:50:11 +08:00
digest = Digest :: MD5 . hexdigest ( warning )
redis_key = " deprecate-notice- #{ digest } "
2019-12-03 17:05:53 +08:00
if ! Discourse . redis . without_namespace . get ( redis_key )
2020-05-10 20:05:23 +08:00
Rails . logger . warn ( warning )
2019-06-21 22:08:57 +08:00
begin
2019-12-03 17:05:53 +08:00
Discourse . redis . without_namespace . setex ( redis_key , 3600 , " x " )
2019-06-21 22:08:57 +08:00
rescue Redis :: CommandError = > e
raise unless e . message =~ / READONLY /
end
2018-06-20 15:50:11 +08:00
end
warning
end
2020-04-30 14:48:34 +08:00
SIDEKIQ_NAMESPACE || = 'sidekiq'
2016-12-05 11:46:34 +08:00
2014-04-23 09:01:17 +08:00
def self . sidekiq_redis_config
2015-06-25 14:51:48 +08:00
conf = GlobalSetting . redis_config . dup
2016-12-05 11:46:34 +08:00
conf [ :namespace ] = SIDEKIQ_NAMESPACE
2015-06-25 14:51:48 +08:00
conf
2014-03-28 10:48:14 +08:00
end
2014-07-29 22:40:02 +08:00
def self . static_doc_topic_ids
[ SiteSetting . tos_topic_id , SiteSetting . guidelines_topic_id , SiteSetting . privacy_topic_id ]
end
2017-02-18 01:09:53 +08:00
cattr_accessor :last_ar_cache_reset
def self . reset_active_record_cache_if_needed ( e )
last_cache_reset = Discourse . last_ar_cache_reset
if e && e . message =~ / UndefinedColumn / && ( last_cache_reset . nil? || last_cache_reset < 30 . seconds . ago )
2018-01-19 05:32:15 +08:00
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. "
2017-02-18 01:09:53 +08:00
Discourse . last_ar_cache_reset = Time . zone . now
Discourse . reset_active_record_cache
end
end
def self . reset_active_record_cache
ActiveRecord :: Base . connection . query_cache . clear
2017-08-17 18:27:35 +08:00
( ActiveRecord :: Base . connection . tables - %w[ schema_migrations versions ] ) . each do | table |
2017-02-18 01:09:53 +08:00
table . classify . constantize . reset_column_information rescue nil
end
nil
end
2017-11-16 05:39:11 +08:00
def self . running_in_rack?
ENV [ " DISCOURSE_RUNNING_IN_RACK " ] == " 1 "
end
2018-10-09 13:11:45 +08:00
def self . skip_post_deployment_migrations?
[ '1' , 'true' ] . include? ( ENV [ " SKIP_POST_DEPLOYMENT_MIGRATIONS " ] & . to_s )
end
2019-10-07 12:33:37 +08:00
# this is used to preload as much stuff as possible prior to forking
# in turn this can conserve large amounts of memory on forking servers
def self . preload_rails!
return if @preloaded_rails
2021-04-30 18:32:13 +08:00
if ! Rails . env . development?
# Skipped in development because the schema cache gets reset on every code change anyway
# Better to rely on the filesystem-based db:schema:cache:dump
2019-10-07 12:33:37 +08:00
2021-04-30 18:32:13 +08:00
# load up all models and schema
( ActiveRecord :: Base . connection . tables - %w[ schema_migrations versions ] ) . each do | table |
table . classify . constantize . first rescue nil
end
# ensure we have a full schema cache in case we missed something above
ActiveRecord :: Base . connection . data_sources . each do | table |
ActiveRecord :: Base . connection . schema_cache . add ( table )
end
2019-10-07 12:33:37 +08:00
end
schema_cache = ActiveRecord :: Base . connection . schema_cache
2020-06-03 15:36:50 +08:00
RailsMultisite :: ConnectionManagement . safe_each_connection do
2021-06-01 14:57:24 +08:00
# load up schema cache for all multisite assuming all dbs have
# an identical schema
2019-10-07 12:33:37 +08:00
dup_cache = schema_cache . dup
# this line is not really needed, but just in case the
# underlying implementation changes lets give it a shot
dup_cache . connection = nil
ActiveRecord :: Base . connection . schema_cache = dup_cache
I18n . t ( :posts )
# this will force Cppjieba to preload if any site has it
# enabled allowing it to be reused between all child processes
Search . prepare_data ( " test " )
2021-05-13 14:16:01 +08:00
JsLocaleHelper . load_translations ( SiteSetting . default_locale )
2021-06-02 13:25:12 +08:00
Site . json_for ( Guardian . new )
2021-06-03 16:14:56 +08:00
SvgSprite . preload
2021-06-08 11:15:55 +08:00
begin
SiteSetting . client_settings_json
rescue = > e
# Rescue from Redis related errors so that we can still boot the
# application even if Redis is down.
warn_exception ( e , message : " Error while preloading client settings json " )
end
2019-10-07 12:33:37 +08:00
end
2021-05-07 13:25:31 +08:00
[
Thread . new {
# router warm up
Rails . application . routes . recognize_path ( 'abc' ) rescue nil
} ,
Thread . new {
# preload discourse version
Discourse . git_version
Discourse . git_branch
Discourse . full_version
} ,
Thread . new {
require 'actionview_precompiler'
ActionviewPrecompiler . precompile
} ,
Thread . new {
LetterAvatar . image_magick_version
2021-06-01 14:57:24 +08:00
} ,
Thread . new {
SvgSprite . core_svgs
2021-05-07 13:25:31 +08:00
}
] . each ( & :join )
2019-10-07 12:33:37 +08:00
ensure
@preloaded_rails = true
end
2019-12-03 17:05:53 +08:00
def self . redis
$redis
end
2019-12-18 13:51:57 +08:00
def self . is_parallel_test?
ENV [ 'RAILS_ENV' ] == " test " && ENV [ 'TEST_ENV_NUMBER' ]
end
2021-01-29 10:14:49 +08:00
CDN_REQUEST_METHODS || = [ " GET " , " HEAD " , " OPTIONS " ]
def self . is_cdn_request? ( env , request_method )
return unless CDN_REQUEST_METHODS . include? ( request_method )
cdn_hostnames = GlobalSetting . cdn_hostnames
return if cdn_hostnames . blank?
requested_hostname = env [ REQUESTED_HOSTNAME ] || env [ Rack :: HTTP_HOST ]
cdn_hostnames . include? ( requested_hostname )
end
def self . apply_cdn_headers ( headers )
headers [ 'Access-Control-Allow-Origin' ] = '*'
headers [ 'Access-Control-Allow-Methods' ] = CDN_REQUEST_METHODS . join ( " , " )
headers
end
2021-07-20 14:55:59 +08:00
def self . allow_dev_populate?
Rails . env . development? || ENV [ " ALLOW_DEV_POPULATE " ] == " 1 "
end
2013-02-06 03:16:51 +08:00
end
2019-12-03 17:05:53 +08:00
# rubocop:enable Style/GlobalVars