2019-05-02 17:17:27 -05:00
# frozen_string_literal: true
2019-12-03 03:05:53 -06:00
# rubocop:disable Style/GlobalVars
2019-05-02 17:17:27 -05:00
2013-05-30 17:41:29 -05:00
require 'cache'
2017-03-17 01:21:30 -05:00
require 'open3'
2017-12-21 15:29:11 -06:00
require_dependency 'route_format'
2013-08-23 01:21:52 -05:00
require_dependency 'plugin/instance'
2013-10-08 23:10:37 -05:00
require_dependency 'auth/default_current_user_provider'
2015-04-27 12:06:53 -05:00
require_dependency 'version'
2017-09-08 12:38:46 -05:00
require 'digest/sha1'
2013-05-30 17:41:29 -05:00
2013-02-05 13:16:51 -06:00
module Discourse
2018-10-08 02:47:38 -05:00
DB_POST_MIGRATE_PATH || = " db/post_migrate "
2021-01-28 20:14:49 -06:00
REQUESTED_HOSTNAME || = " REQUESTED_HOSTNAME "
2013-02-05 13:16:51 -06:00
2014-04-17 00:57:17 -05:00
require 'sidekiq/exception_handler'
2014-02-20 21:30:25 -06:00
class SidekiqExceptionHandler
extend Sidekiq :: ExceptionHandler
end
2017-03-17 01:21:30 -05:00
class Utils
2020-08-07 09:28:43 -05:00
URI_REGEXP || = URI . regexp ( %w{ http https } )
2020-08-05 23:25:03 -05:00
2019-11-07 09:47:16 -06: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 01:21:30 -05:00
2019-11-07 09:47:16 -06: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 01:21:30 -05:00
end
end
def self . pretty_logs ( logs )
2020-04-30 01:48:34 -05:00
logs . join ( " \n " )
2017-03-17 01:21:30 -05:00
end
2019-11-07 09:47:16 -06:00
2021-08-03 12:06:50 -05: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-04 11:28:26 -06: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 05:20:26 -05:00
FileUtils . mv ( temp_destination , destination )
2020-03-04 11:28:26 -06: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 05:20:26 -05:00
FileUtils . mv ( temp_destination , destination )
2020-03-04 11:28:26 -06:00
nil
end
2019-11-07 09:47:16 -06: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 10:29:37 -05:00
def execute_command ( * command , timeout : nil , failure_message : " " , success_status_codes : [ 0 ] , chdir : " . " , unsafe_shell : false )
2021-04-12 07:53:41 -05:00
env = nil
env = command . shift if command [ 0 ] . is_a? ( Hash )
2021-04-15 10:29:37 -05: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-11 22:55:54 -05: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 07:53:41 -05:00
args = command
args = [ env ] + command if env
stdout , stderr , status = Open3 . capture3 ( * args , chdir : chdir )
2019-11-07 09:47:16 -06: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 01:21:30 -05:00
end
2014-07-17 15:22:46 -05:00
# Log an exception.
#
2014-07-17 17:07:25 -05: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-17 15:22:46 -05:00
# See app/jobs/base.rb for the error_context function.
2015-02-09 14:47:46 -06:00
def self . handle_job_exception ( ex , context = { } , parent_logger = nil )
2018-07-31 16:12:55 -05:00
return if ex . class == Jobs :: HandledExceptionWrapper
2014-02-20 21:30:25 -06: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 09:57:47 -05:00
raise ex if Rails . env . test?
2014-02-20 21:30:25 -06:00
end
2013-06-18 19:31:19 -05:00
# Expected less matches than what we got in a find
2015-03-22 20:16:21 -05:00
class TooManyMatches < StandardError ; end
2013-06-18 19:31:19 -05:00
2013-02-25 10:42:20 -06:00
# When they try to do something they should be logged in for
2015-03-22 20:16:21 -05:00
class NotLoggedIn < StandardError ; end
2013-02-05 13:16:51 -06:00
# When the input is somehow bad
2015-03-22 20:16:21 -05:00
class InvalidParameters < StandardError ; end
2013-02-05 13:16:51 -06:00
# When they don't have permission to do something
2015-09-18 02:14:10 -05:00
class InvalidAccess < StandardError
2019-10-08 06:15:08 -05:00
attr_reader :obj
attr_reader :opts
attr_reader :custom_message
2020-11-24 05:06:52 -06:00
attr_reader :custom_message_params
2019-10-08 06:15:08 -05:00
attr_reader :group
2017-09-23 09:39:58 -05:00
def initialize ( msg = nil , obj = nil , opts = nil )
2015-09-18 02:14:10 -05:00
super ( msg )
2017-09-23 09:39:58 -05:00
2018-02-09 18:09:54 -06:00
@opts = opts || { }
2015-09-18 02:14:10 -05:00
@obj = obj
2019-10-08 06:15:08 -05:00
@custom_message = opts [ :custom_message ] if @opts [ :custom_message ]
2020-11-24 05:06:52 -06:00
@custom_message_params = opts [ :custom_message_params ] if @opts [ :custom_message_params ]
2019-10-08 06:15:08 -05:00
@group = opts [ :group ] if @opts [ :group ]
2015-09-18 02:14:10 -05:00
end
end
2013-02-05 13:16:51 -06:00
# When something they want is not found
2018-08-09 00:05:12 -05:00
class NotFound < StandardError
attr_reader :status
attr_reader :check_permalinks
attr_reader :original_path
2019-10-08 06:15:08 -05: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 00:05:12 -05:00
@status = status
@check_permalinks = check_permalinks
@original_path = original_path
2019-10-08 06:15:08 -05:00
@custom_message = custom_message
2018-08-09 00:05:12 -05:00
end
end
2013-02-05 13:16:51 -06:00
2013-06-04 17:34:53 -05:00
# When a setting is missing
2015-03-22 20:16:21 -05:00
class SiteSettingMissing < StandardError ; end
2013-06-04 17:34:53 -05:00
2013-11-05 12:04:47 -06:00
# When ImageMagick is missing
2015-03-22 20:16:21 -05:00
class ImageMagickMissing < StandardError ; end
2013-11-05 12:04:47 -06:00
2014-02-12 22:37:28 -06:00
# When read-only mode is enabled
2015-03-22 20:16:21 -05:00
class ReadOnly < StandardError ; end
2014-02-12 22:37:28 -06:00
2013-07-29 00:13:13 -05:00
# Cross site request forgery
2015-03-22 20:16:21 -05:00
class CSRF < StandardError ; end
2013-07-29 00:13:13 -05:00
2017-08-06 20:43:09 -05:00
class Deprecation < StandardError ; end
2019-02-07 08:27:42 -06:00
class ScssError < StandardError ; end
2013-12-23 17:50:36 -06:00
def self . filters
2021-08-10 09:30:34 -05:00
@filters || = [ :latest , :unread , :new , :unseen , :top , :read , :posted , :bookmarks ]
2013-12-23 17:50:36 -06:00
end
def self . anonymous_filters
2015-07-27 01:46:50 -05:00
@anonymous_filters || = [ :latest , :top , :categories ]
2013-12-23 17:50:36 -06:00
end
def self . top_menu_items
2020-07-22 08:56:36 -05:00
@top_menu_items || = Discourse . filters + [ :categories ]
2013-12-23 17:50:36 -06:00
end
def self . anonymous_top_menu_items
2019-11-11 07:18:10 -06:00
@anonymous_top_menu_items || = Discourse . anonymous_filters + [ :categories , :top ]
2013-12-23 17:50:36 -06:00
end
2016-04-06 03:57:59 -05:00
PIXEL_RATIOS || = [ 1 , 1 . 5 , 2 , 3 ]
2015-05-29 02:57:54 -05:00
2015-05-25 10:59:00 -05:00
def self . avatar_sizes
2015-05-29 02:57:54 -05: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 10:03:20 -05:00
set << ( size * pixel_ratio ) . to_i
2015-05-29 02:57:54 -05:00
end
end
2015-05-26 00:41:50 -05:00
set
2015-05-25 10:59:00 -05:00
end
2013-08-01 00:59:57 -05:00
def self . activate_plugins!
2015-04-27 12:06:53 -05:00
@plugins = [ ]
2019-11-18 17:15:09 -06:00
Plugin :: Instance . find_all ( " #{ Rails . root } /plugins " ) . each do | p |
2015-04-27 12:06:53 -05: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-10 14:06:15 -06:00
DiscourseEvent . trigger ( :after_plugin_activation )
2013-08-01 00:59:57 -05:00
end
2015-02-04 15:23:39 -06:00
def self . disabled_plugin_names
2016-06-30 09:55:01 -05:00
plugins . select { | p | ! p . enabled? } . map ( & :name )
2015-02-04 15:23:39 -06:00
end
2013-08-01 00:59:57 -05:00
def self . plugins
2015-02-10 10:18:16 -06:00
@plugins || = [ ]
2013-08-01 00:59:57 -05:00
end
2018-05-08 00:24:58 -05:00
def self . hidden_plugins
@hidden_plugins || = [ ]
end
2018-05-08 18:52:21 -05:00
def self . visible_plugins
2018-05-08 00:24:58 -05:00
self . plugins - self . hidden_plugins
end
2017-01-12 14:43:09 -06:00
def self . plugin_themes
@plugin_themes || = plugins . map ( & :themes ) . flatten
end
2016-11-14 18:42:55 -06: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 09:52:54 -05: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 04:50:31 -05:00
next if ! args [ :include_disabled ] && ! plugin . enabled?
2019-07-15 09:52:54 -05:00
true
end
end
2021-04-23 09:24:42 -05: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 10:30:31 -05:00
end
2021-04-23 09:24:42 -05: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 10:30:31 -05:00
2019-09-16 08:56:19 -05: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-21 22:09:10 -05:00
assets
2019-08-20 11:39:52 -05:00
end
2019-07-15 09:52:54 -05:00
def self . find_plugin_js_assets ( args )
2020-03-13 10:30:31 -05:00
plugins = self . find_plugins ( args ) . select do | plugin |
2019-07-15 09:52:54 -05:00
plugin . js_asset_exists?
2020-03-13 10:30:31 -05:00
end
2021-04-23 09:24:42 -05:00
plugins = apply_asset_filters ( plugins , :js , args [ :request ] )
2020-03-13 10:30:31 -05:00
plugins . map { | plugin | " plugins/ #{ plugin . directory_name } " }
2019-07-15 09:52:54 -05:00
end
2014-01-14 19:07:42 -06: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-03 21:21:00 -05:00
message = MessageBus . last_message ( channel )
2014-01-14 19:07:42 -06:00
unless message && message . data == digest
2015-05-03 21:21:00 -05:00
MessageBus . publish channel , digest
2014-01-14 19:07:42 -06:00
end
digest
end
end
2018-08-09 10:29:02 -05:00
BUILTIN_AUTH || = [
2019-03-27 08:25:04 -05: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 06:10:43 -05:00
Auth :: AuthProvider . new ( authenticator : Auth :: DiscordAuthenticator . new , icon : " fab-discord " )
2018-07-31 10:18:50 -05: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-25 20:04:16 -05: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 10:18:50 -05:00
auth_providers . map ( & :authenticator )
2013-08-25 20:04:16 -05:00
end
2018-07-23 10:51:57 -05:00
def self . enabled_authenticators
authenticators . select { | authenticator | authenticator . enabled? }
2013-08-01 00:59:57 -05:00
end
2013-05-30 17:41:29 -05:00
def self . cache
2019-06-12 21:58:27 -05:00
@cache || = begin
if GlobalSetting . skip_redis?
ActiveSupport :: Cache :: MemoryStore . new
else
Cache . new
end
end
2013-05-30 17:41:29 -05:00
end
2013-02-05 13:16:51 -06:00
2020-02-17 22:11:30 -06: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-05 13:16:51 -06:00
# Get the current base URL for the current site
def self . current_hostname
2016-06-30 09:55:01 -05:00
SiteSetting . force_hostname . presence || RailsMultisite :: ConnectionManagement . current_hostname
2013-05-30 17:41:29 -05:00
end
2020-10-09 06:51:24 -05:00
def self . base_path ( default_value = " " )
2016-06-30 09:55:01 -05:00
ActionController :: Base . config . relative_url_root . presence || default_value
2013-03-14 07:01:52 -05:00
end
2020-10-09 06:51:24 -05: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-28 12:54:17 -05:00
def self . base_protocol
SiteSetting . force_https? ? " https " : " http "
end
2013-05-30 17:41:29 -05:00
def self . base_url_no_prefix
2016-07-28 12:54:17 -05:00
default_port = SiteSetting . force_https? ? 443 : 80
2019-05-02 17:17:27 -05:00
url = + " #{ base_protocol } :// #{ current_hostname } "
2016-06-30 09:55:01 -05:00
url << " : #{ SiteSetting . port } " if SiteSetting . port . to_i > 0 && SiteSetting . port . to_i != default_port
2019-05-06 00:26:57 -05:00
if Rails . env . development? && SiteSetting . port . blank?
url << " : #{ ENV [ " UNICORN_PORT " ] || 3000 } "
end
2016-06-30 09:55:01 -05:00
url
2013-04-05 05:38:20 -05:00
end
2013-05-30 17:41:29 -05:00
def self . base_url
2020-10-09 06:51:24 -05:00
base_url_no_prefix + base_path
2013-05-30 17:41:29 -05:00
end
2017-07-19 14:08:54 -05:00
def self . route_for ( uri )
2018-03-28 03:20:08 -05:00
unless uri . is_a? ( URI )
uri = begin
URI ( uri )
2020-11-20 03:28:14 -06:00
rescue ArgumentError , URI :: Error
2018-03-28 03:20:08 -05:00
end
end
2017-07-19 14:08:54 -05:00
return unless uri
2019-05-02 17:17:27 -05:00
path = + ( uri . path || " " )
2020-10-09 06:51:24 -05:00
if ! uri . host || ( uri . host == Discourse . current_hostname && path . start_with? ( Discourse . base_path ) )
path . slice! ( Discourse . base_path )
2017-07-19 14:08:54 -05:00
return Rails . application . routes . recognize_path ( path )
end
2017-07-20 15:01:16 -05:00
nil
rescue ActionController :: RoutingError
2017-07-19 14:08:54 -05:00
nil
end
2018-11-06 07:17:13 -06:00
class << self
alias_method :base_url_no_path , :base_url_no_prefix
end
2020-06-11 00:45:46 -05:00
READONLY_MODE_KEY_TTL || = 60
READONLY_MODE_KEY || = 'readonly_mode'
PG_READONLY_MODE_KEY || = 'readonly_mode:postgres'
2020-07-14 03:15:58 -05:00
PG_READONLY_MODE_KEY_TTL || = 300
2020-06-11 00:45:46 -05:00
USER_READONLY_MODE_KEY || = 'readonly_mode:user'
PG_FORCE_READONLY_MODE_KEY || = 'readonly_mode:postgres_force'
2016-06-29 01:19:18 -05:00
2017-01-11 04:03:36 -06:00
READONLY_KEYS || = [
2017-01-11 02:38:07 -06:00
READONLY_MODE_KEY ,
PG_READONLY_MODE_KEY ,
2020-06-11 00:45:46 -05:00
USER_READONLY_MODE_KEY ,
PG_FORCE_READONLY_MODE_KEY
2017-01-11 02:38:07 -06:00
]
def self . enable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 04:27:24 -06:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . pause! ( " pg_failover " ) if ! Sidekiq . paused?
end
2020-06-11 00:45:46 -05:00
if key == USER_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
2019-12-03 03:05:53 -06:00
Discourse . redis . set ( key , 1 )
2016-06-29 01:19:18 -05:00
else
2020-07-14 03:15:58 -05: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 01:19:18 -05:00
end
2016-06-29 00:55:17 -05:00
2015-05-03 21:21:00 -05:00
MessageBus . publish ( readonly_channel , true )
2013-02-05 13:16:51 -06:00
true
end
2020-07-14 03:15:58 -05:00
def self . keep_readonly_mode ( key , ttl : )
# extend the expiry by ttl minute every ttl/2 seconds
2019-02-19 20:01:18 -06:00
@mutex || = Mutex . new
@mutex . synchronize do
2018-06-19 02:44:08 -05:00
@dbs || = Set . new
@dbs << RailsMultisite :: ConnectionManagement . current_db
2018-06-18 21:15:29 -05:00
@threads || = { }
2018-06-19 02:44:08 -05:00
unless @threads [ key ] & . alive?
2018-06-18 21:15:29 -05:00
@threads [ key ] = Thread . new do
2019-02-19 20:01:18 -06:00
while @dbs . size > 0 do
2020-07-14 03:15:58 -05:00
sleep ttl / 2
2018-06-21 04:52:42 -05:00
2019-02-19 20:01:18 -06:00
@mutex . synchronize do
@dbs . each do | db |
RailsMultisite :: ConnectionManagement . with_connection ( db ) do
2020-07-14 03:15:58 -05:00
if ! Discourse . redis . expire ( key , ttl )
2019-02-19 20:01:18 -06:00
@dbs . delete ( db )
end
2018-06-19 02:44:08 -05:00
end
end
end
2018-06-18 21:15:29 -05:00
end
2016-11-10 09:44:51 -06:00
end
2015-02-11 14:50:17 -06:00
end
end
end
2017-01-11 02:38:07 -06:00
def self . disable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 04:27:24 -06:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . unpause! if Sidekiq . paused?
end
2019-12-03 03:05:53 -06:00
Discourse . redis . del ( key )
2015-05-03 21:21:00 -05:00
MessageBus . publish ( readonly_channel , false )
2013-02-05 13:16:51 -06:00
true
end
2020-06-11 00:45:46 -05: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-11 11:21:29 -05:00
def self . readonly_mode? ( keys = READONLY_KEYS )
2020-06-14 20:57:44 -05:00
recently_readonly? || Discourse . redis . exists? ( * keys )
2017-01-11 02:38:07 -06:00
end
2019-01-20 23:29:29 -06:00
def self . pg_readonly_mode?
2019-12-03 03:05:53 -06:00
Discourse . redis . get ( PG_READONLY_MODE_KEY ) . present?
2019-01-20 23:29:29 -06:00
end
2019-06-21 09:08:57 -05: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 02:38:07 -06:00
end
def self . recently_readonly?
2019-12-03 03:05:53 -06:00
postgres_read_only = postgres_last_read_only [ Discourse . redis . namespace ]
redis_read_only = redis_last_read_only [ Discourse . redis . namespace ]
2019-06-21 09:08:57 -05:00
( redis_read_only . present? && redis_read_only > 15 . seconds . ago ) ||
( postgres_read_only . present? && postgres_read_only > 15 . seconds . ago )
2017-01-11 02:38:07 -06:00
end
2019-06-21 09:08:57 -05:00
def self . received_postgres_readonly!
2019-12-03 03:05:53 -06:00
postgres_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2019-06-21 09:08:57 -05:00
end
2020-06-09 03:36:04 -05:00
def self . clear_postgres_readonly!
postgres_last_read_only [ Discourse . redis . namespace ] = nil
end
2019-06-21 09:08:57 -05:00
def self . received_redis_readonly!
2019-12-03 03:05:53 -06:00
redis_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2017-01-11 02:38:07 -06:00
end
2020-06-09 03:36:04 -05:00
def self . clear_redis_readonly!
redis_last_read_only [ Discourse . redis . namespace ] = nil
end
2017-01-11 02:38:07 -06:00
def self . clear_readonly!
2020-06-09 03:36:04 -05:00
clear_redis_readonly!
clear_postgres_readonly!
2019-01-21 19:51:45 -06:00
Site . clear_anon_cache!
true
2013-02-05 13:16:51 -06:00
end
2017-08-15 21:38:30 -05:00
def self . request_refresh! ( user_ids : nil )
2014-02-20 23:52:11 -06:00
# Causes refresh on next click for all clients
#
2015-05-03 21:21:00 -05:00
# This is better than `MessageBus.publish "/file-change", ["refresh"]` because
2014-02-20 23:52:11 -06:00
# it spreads the refreshes out over a time period
2017-08-15 21:38:30 -05:00
if user_ids
2017-08-15 23:06:47 -05:00
MessageBus . publish ( " /refresh_client " , 'clobber' , user_ids : user_ids )
2017-08-15 21:38:30 -05:00
else
MessageBus . publish ( '/global/asset-version' , 'clobber' )
end
2014-02-20 23:52:11 -06:00
end
2017-10-03 22:22:23 -05: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-02 16:25:57 -05:00
2017-10-03 22:22:23 -05: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 03:05:53 -06:00
end # rubocop:disable Style/GlobalVars
2013-02-18 00:39:54 -06:00
end
2014-09-09 16:04:10 -05:00
def self . git_branch
2017-10-03 22:22:23 -05: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-28 11:24:56 -05:00
end
def self . full_version
2017-10-03 22:22:23 -05: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-28 11:24:56 -05:00
end
2019-05-17 00:42:45 -05: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-03 22:22:23 -05:00
def self . try_git ( git_cmd , default_value )
2017-08-28 11:24:56 -05:00
version_value = false
2014-09-09 16:04:10 -05:00
2017-10-03 22:22:23 -05:00
begin
version_value = ` #{ git_cmd } ` . strip
rescue
version_value = default_value
2014-09-09 16:04:10 -05:00
end
2017-08-28 11:24:56 -05:00
if version_value . empty?
version_value = default_value
end
version_value
2014-09-09 16:04:10 -05:00
end
2013-09-06 02:28:37 -05:00
# Either returns the site_contact_username user or the first admin.
def self . site_contact_user
2014-05-06 08:41:59 -05:00
user = User . find_by ( username_lower : SiteSetting . site_contact_username . downcase ) if SiteSetting . site_contact_username . present?
2015-11-24 13:37:33 -06:00
user || = ( system_user || User . admins . real . order ( :id ) . first )
2013-05-30 17:41:29 -05:00
end
2013-02-05 13:16:51 -06:00
2015-05-06 18:00:13 -05:00
SYSTEM_USER_ID || = - 1
2014-06-24 19:45:20 -05:00
2013-09-06 02:28:37 -05:00
def self . system_user
2019-10-31 10:16:26 -05:00
@system_users || = { }
current_db = RailsMultisite :: ConnectionManagement . current_db
@system_users [ current_db ] || = User . find_by ( id : SYSTEM_USER_ID )
2013-09-06 02:28:37 -05:00
end
2013-07-31 16:26:34 -05:00
def self . store
2017-10-06 00:20:01 -05:00
if SiteSetting . Upload . enable_s3_uploads
2013-07-31 16:26:34 -05:00
@s3_store_loaded || = require 'file_store/s3_store'
2013-11-05 12:04:47 -06:00
FileStore :: S3Store . new
2013-07-31 16:26:34 -05:00
else
@local_store_loaded || = require 'file_store/local_store'
2013-11-05 12:04:47 -06:00
FileStore :: LocalStore . new
2013-07-31 16:26:34 -05:00
end
end
2019-04-17 02:15:04 -05:00
def self . stats
2019-05-01 13:04:18 -05:00
PluginStore . new ( " stats " )
2019-04-17 02:15:04 -05:00
end
2013-10-08 23:10:37 -05: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-05 12:04:47 -06:00
def self . asset_host
Rails . configuration . action_controller . asset_host
end
2014-02-12 22:37:28 -06:00
def self . readonly_channel
2014-02-19 11:21:41 -06:00
" /site/read-only "
2013-02-05 13:16:51 -06:00
end
2014-02-12 22:37:28 -06:00
2014-03-27 21:48:14 -05:00
# all forking servers must call this
# after fork, otherwise Discourse will be
# in a bad state
def self . after_fork
2018-06-14 03:22:02 -05:00
# note: some of this reconnecting may no longer be needed per https://github.com/redis/redis-rb/pull/414
2015-05-03 21:21:00 -05:00
MessageBus . after_fork
2014-03-27 21:48:14 -05:00
SiteSetting . after_fork
2020-05-31 21:55:53 -05:00
Discourse . redis . reconnect
2014-03-27 21:48:14 -05:00
Rails . cache . reconnect
2019-11-26 19:35:14 -06:00
Discourse . cache . reconnect
2014-05-07 17:05:28 -05:00
Logster . store . redis . reconnect
2014-04-22 20:01:17 -05:00
# shuts down all connections in the pool
2020-06-11 01:09:19 -05:00
Sidekiq . redis_pool . shutdown { | conn | conn . disconnect! }
2014-04-22 20:01:17 -05:00
# re-establish
Sidekiq . redis = sidekiq_redis_config
2016-07-16 00:11:34 -05:00
# in case v8 was initialized we want to make sure it is nil
PrettyText . reset_context
2016-11-01 21:34:20 -05:00
2020-03-11 08:43:55 -05:00
DiscourseJsProcessor :: Transpiler . reset_context if defined? DiscourseJsProcessor :: Transpiler
2016-11-01 21:34:20 -05:00
JsLocaleHelper . reset_context if defined? JsLocaleHelper
2021-06-03 01:41:16 -05: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-07 17:05:28 -05:00
nil
2014-04-22 20:01:17 -05:00
end
2018-08-12 22:14:34 -05: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 01:33:06 -05:00
logster_env = env
2018-08-12 22:14:34 -05:00
if old_env = Thread . current [ Logster :: Logger :: LOGSTER_ENV ]
2018-08-13 01:33:06 -05: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-12 22:14:34 -05: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 01:33:06 -05:00
env : logster_env
2018-08-12 22:14:34 -05:00
)
end
2018-08-13 01:33:06 -05:00
if old_env
env . each do | k , v |
# do not leak state
logster_env . delete ( k )
end
end
nil
2018-08-12 22:14:34 -05:00
end
2017-11-30 23:23:21 -06: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-04 16:54:28 -06:00
env || = { }
env [ :current_db ] || = RailsMultisite :: ConnectionManagement . current_db
2017-11-30 23:23:21 -06: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-10 21:49:46 -05:00
Rails . logger . warn ( " #{ message } #{ e } \n #{ e . backtrace . join ( " \n " ) } " )
2017-11-30 23:23:21 -06:00
end
rescue
STDERR . puts " Failed to report exception #{ e } #{ message } "
end
2019-01-03 11:03:01 -06:00
def self . deprecate ( warning , drop_from : nil , since : nil , raise_error : false , output_in_test : false )
2018-12-06 05:38:01 -06: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 02:50:11 -05:00
if Rails . env == " development "
STDERR . puts ( warning )
end
2019-01-03 11:03:01 -06:00
if output_in_test && Rails . env == " test "
STDERR . puts ( warning )
end
2018-06-20 02:50:11 -05:00
digest = Digest :: MD5 . hexdigest ( warning )
redis_key = " deprecate-notice- #{ digest } "
2019-12-03 03:05:53 -06:00
if ! Discourse . redis . without_namespace . get ( redis_key )
2020-05-10 07:05:23 -05:00
Rails . logger . warn ( warning )
2019-06-21 09:08:57 -05:00
begin
2019-12-03 03:05:53 -06:00
Discourse . redis . without_namespace . setex ( redis_key , 3600 , " x " )
2019-06-21 09:08:57 -05:00
rescue Redis :: CommandError = > e
raise unless e . message =~ / READONLY /
end
2018-06-20 02:50:11 -05:00
end
warning
end
2020-04-30 01:48:34 -05:00
SIDEKIQ_NAMESPACE || = 'sidekiq'
2016-12-04 21:46:34 -06:00
2014-04-22 20:01:17 -05:00
def self . sidekiq_redis_config
2015-06-25 01:51:48 -05:00
conf = GlobalSetting . redis_config . dup
2016-12-04 21:46:34 -06:00
conf [ :namespace ] = SIDEKIQ_NAMESPACE
2015-06-25 01:51:48 -05:00
conf
2014-03-27 21:48:14 -05:00
end
2014-07-29 09:40:02 -05:00
def self . static_doc_topic_ids
[ SiteSetting . tos_topic_id , SiteSetting . guidelines_topic_id , SiteSetting . privacy_topic_id ]
end
2017-02-17 11:09:53 -06: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-18 15:32:15 -06: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-17 11:09:53 -06: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 05:27:35 -05:00
( ActiveRecord :: Base . connection . tables - %w[ schema_migrations versions ] ) . each do | table |
2017-02-17 11:09:53 -06:00
table . classify . constantize . reset_column_information rescue nil
end
nil
end
2017-11-15 15:39:11 -06:00
def self . running_in_rack?
ENV [ " DISCOURSE_RUNNING_IN_RACK " ] == " 1 "
end
2018-10-09 00:11:45 -05:00
def self . skip_post_deployment_migrations?
[ '1' , 'true' ] . include? ( ENV [ " SKIP_POST_DEPLOYMENT_MIGRATIONS " ] & . to_s )
end
2019-10-06 23:33:37 -05: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 05:32:13 -05: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-06 23:33:37 -05:00
2021-04-30 05:32:13 -05: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-06 23:33:37 -05:00
end
schema_cache = ActiveRecord :: Base . connection . schema_cache
2020-06-03 02:36:50 -05:00
RailsMultisite :: ConnectionManagement . safe_each_connection do
2021-06-01 01:57:24 -05:00
# load up schema cache for all multisite assuming all dbs have
# an identical schema
2019-10-06 23:33:37 -05: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 01:16:01 -05:00
JsLocaleHelper . load_translations ( SiteSetting . default_locale )
2021-06-02 00:25:12 -05:00
Site . json_for ( Guardian . new )
2021-06-03 03:14:56 -05:00
SvgSprite . preload
2021-06-07 22:15:55 -05: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-06 23:33:37 -05:00
end
2021-05-07 00:25:31 -05: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 01:57:24 -05:00
} ,
Thread . new {
SvgSprite . core_svgs
2021-05-07 00:25:31 -05:00
}
] . each ( & :join )
2019-10-06 23:33:37 -05:00
ensure
@preloaded_rails = true
end
2019-12-03 03:05:53 -06:00
def self . redis
$redis
end
2019-12-17 23:51:57 -06:00
def self . is_parallel_test?
ENV [ 'RAILS_ENV' ] == " test " && ENV [ 'TEST_ENV_NUMBER' ]
end
2021-01-28 20:14:49 -06: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 01:55:59 -05:00
def self . allow_dev_populate?
Rails . env . development? || ENV [ " ALLOW_DEV_POPULATE " ] == " 1 "
end
2013-02-05 13:16:51 -06:00
end
2019-12-03 03:05:53 -06:00
# rubocop:enable Style/GlobalVars