2019-05-02 18:17:27 -04:00
# frozen_string_literal: true
2013-05-30 18:41:29 -04:00
require 'cache'
2017-03-17 02:21:30 -04:00
require 'open3'
2022-03-21 10:28:52 -04:00
require 'plugin/instance'
require 'version'
2013-05-30 18:41:29 -04:00
2013-02-05 14:16:51 -05:00
module Discourse
2018-10-08 03:47:38 -04:00
DB_POST_MIGRATE_PATH || = " db/post_migrate "
2021-01-28 21:14:49 -05:00
REQUESTED_HOSTNAME || = " REQUESTED_HOSTNAME "
2013-02-05 14:16:51 -05:00
2017-03-17 02:21:30 -04:00
class Utils
2020-08-07 10:28:43 -04:00
URI_REGEXP || = URI . regexp ( %w{ http https } )
2020-08-06 00:25:03 -04:00
2019-11-07 10:47:16 -05: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 02:21:30 -04:00
2019-11-07 10:47:16 -05: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 02:21:30 -04:00
end
end
def self . pretty_logs ( logs )
2020-04-30 02:48:34 -04:00
logs . join ( " \n " )
2017-03-17 02:21:30 -04:00
end
2019-11-07 10:47:16 -05:00
2021-08-03 13:06:50 -04: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 12:28:26 -05: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 06:20:26 -04:00
FileUtils . mv ( temp_destination , destination )
2020-03-04 12:28:26 -05: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 06:20:26 -04:00
FileUtils . mv ( temp_destination , destination )
2020-03-04 12:28:26 -05:00
nil
end
2019-11-07 10:47:16 -05: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 11:29:37 -04:00
def execute_command ( * command , timeout : nil , failure_message : " " , success_status_codes : [ 0 ] , chdir : " . " , unsafe_shell : false )
2021-04-12 08:53:41 -04:00
env = nil
env = command . shift if command [ 0 ] . is_a? ( Hash )
2021-04-15 11:29:37 -04: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 23:55:54 -04: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 08:53:41 -04:00
args = command
args = [ env ] + command if env
stdout , stderr , status = Open3 . capture3 ( * args , chdir : chdir )
2019-11-07 10:47:16 -05: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 02:21:30 -04:00
end
2022-08-02 22:53:26 -04:00
def self . job_exception_stats
@job_exception_stats
end
def self . reset_job_exception_stats!
@job_exception_stats = Hash . new ( 0 )
end
reset_job_exception_stats!
2014-07-17 16:22:46 -04:00
# Log an exception.
#
2014-07-17 18:07:25 -04: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 16:22:46 -04:00
# See app/jobs/base.rb for the error_context function.
2015-02-09 15:47:46 -05:00
def self . handle_job_exception ( ex , context = { } , parent_logger = nil )
2018-07-31 17:12:55 -04:00
return if ex . class == Jobs :: HandledExceptionWrapper
2014-02-20 22:30:25 -05:00
context || = { }
2022-06-20 21:23:36 -04:00
parent_logger || = Sidekiq
2014-02-20 22:30:25 -05:00
2022-08-03 00:28:46 -04:00
job = context [ :job ]
2022-08-05 03:40:22 -04:00
# mini_scheduler direct reporting
2022-08-03 00:28:46 -04:00
if Hash === job
job_class = job [ " class " ]
if job_class
job_exception_stats [ job_class ] += 1
end
2022-08-02 22:53:26 -04:00
end
2022-08-05 03:40:22 -04:00
# internal reporting
if job . class == Class && :: Jobs :: Base > job
job_exception_stats [ job ] += 1
end
2014-02-20 22:30:25 -05:00
cm = RailsMultisite :: ConnectionManagement
parent_logger . handle_exception ( ex , {
current_db : cm . current_db ,
current_hostname : cm . current_hostname
} . merge ( context ) )
2019-04-08 10:57:47 -04:00
raise ex if Rails . env . test?
2014-02-20 22:30:25 -05:00
end
2013-06-18 20:31:19 -04:00
# Expected less matches than what we got in a find
2015-03-22 21:16:21 -04:00
class TooManyMatches < StandardError ; end
2013-06-18 20:31:19 -04:00
2013-02-25 11:42:20 -05:00
# When they try to do something they should be logged in for
2015-03-22 21:16:21 -04:00
class NotLoggedIn < StandardError ; end
2013-02-05 14:16:51 -05:00
# When the input is somehow bad
2015-03-22 21:16:21 -04:00
class InvalidParameters < StandardError ; end
2013-02-05 14:16:51 -05:00
# When they don't have permission to do something
2015-09-18 03:14:10 -04:00
class InvalidAccess < StandardError
2019-10-08 07:15:08 -04:00
attr_reader :obj
attr_reader :opts
attr_reader :custom_message
2020-11-24 06:06:52 -05:00
attr_reader :custom_message_params
2019-10-08 07:15:08 -04:00
attr_reader :group
2017-09-23 10:39:58 -04:00
def initialize ( msg = nil , obj = nil , opts = nil )
2015-09-18 03:14:10 -04:00
super ( msg )
2017-09-23 10:39:58 -04:00
2018-02-09 19:09:54 -05:00
@opts = opts || { }
2015-09-18 03:14:10 -04:00
@obj = obj
2019-10-08 07:15:08 -04:00
@custom_message = opts [ :custom_message ] if @opts [ :custom_message ]
2020-11-24 06:06:52 -05:00
@custom_message_params = opts [ :custom_message_params ] if @opts [ :custom_message_params ]
2019-10-08 07:15:08 -04:00
@group = opts [ :group ] if @opts [ :group ]
2015-09-18 03:14:10 -04:00
end
end
2013-02-05 14:16:51 -05:00
# When something they want is not found
2018-08-09 01:05:12 -04:00
class NotFound < StandardError
attr_reader :status
attr_reader :check_permalinks
attr_reader :original_path
2019-10-08 07:15:08 -04: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 01:05:12 -04:00
@status = status
@check_permalinks = check_permalinks
@original_path = original_path
2019-10-08 07:15:08 -04:00
@custom_message = custom_message
2018-08-09 01:05:12 -04:00
end
end
2013-02-05 14:16:51 -05:00
2013-06-04 18:34:53 -04:00
# When a setting is missing
2015-03-22 21:16:21 -04:00
class SiteSettingMissing < StandardError ; end
2013-06-04 18:34:53 -04:00
2013-11-05 13:04:47 -05:00
# When ImageMagick is missing
2015-03-22 21:16:21 -04:00
class ImageMagickMissing < StandardError ; end
2013-11-05 13:04:47 -05:00
2014-02-12 23:37:28 -05:00
# When read-only mode is enabled
2015-03-22 21:16:21 -04:00
class ReadOnly < StandardError ; end
2014-02-12 23:37:28 -05:00
2013-07-29 01:13:13 -04:00
# Cross site request forgery
2015-03-22 21:16:21 -04:00
class CSRF < StandardError ; end
2013-07-29 01:13:13 -04:00
2017-08-06 21:43:09 -04:00
class Deprecation < StandardError ; end
2019-02-07 09:27:42 -05:00
class ScssError < StandardError ; end
2013-12-23 18:50:36 -05:00
def self . filters
2021-08-10 10:30:34 -04:00
@filters || = [ :latest , :unread , :new , :unseen , :top , :read , :posted , :bookmarks ]
2013-12-23 18:50:36 -05:00
end
def self . anonymous_filters
2015-07-27 02:46:50 -04:00
@anonymous_filters || = [ :latest , :top , :categories ]
2013-12-23 18:50:36 -05:00
end
def self . top_menu_items
2020-07-22 09:56:36 -04:00
@top_menu_items || = Discourse . filters + [ :categories ]
2013-12-23 18:50:36 -05:00
end
def self . anonymous_top_menu_items
2019-11-11 08:18:10 -05:00
@anonymous_top_menu_items || = Discourse . anonymous_filters + [ :categories , :top ]
2013-12-23 18:50:36 -05:00
end
2016-04-06 04:57:59 -04:00
PIXEL_RATIOS || = [ 1 , 1 . 5 , 2 , 3 ]
2015-05-29 03:57:54 -04:00
2015-05-25 11:59:00 -04:00
def self . avatar_sizes
2015-05-29 03:57:54 -04: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 11:03:20 -04:00
set << ( size * pixel_ratio ) . to_i
2015-05-29 03:57:54 -04:00
end
end
2015-05-26 01:41:50 -04:00
set
2015-05-25 11:59:00 -04:00
end
2013-08-01 01:59:57 -04:00
def self . activate_plugins!
2015-04-27 13:06:53 -04:00
@plugins = [ ]
2019-11-18 18:15:09 -05:00
Plugin :: Instance . find_all ( " #{ Rails . root } /plugins " ) . each do | p |
2015-04-27 13:06:53 -04: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 15:06:15 -05:00
DiscourseEvent . trigger ( :after_plugin_activation )
2013-08-01 01:59:57 -04:00
end
2015-02-04 16:23:39 -05:00
def self . disabled_plugin_names
2016-06-30 10:55:01 -04:00
plugins . select { | p | ! p . enabled? } . map ( & :name )
2015-02-04 16:23:39 -05:00
end
2013-08-01 01:59:57 -04:00
def self . plugins
2015-02-10 11:18:16 -05:00
@plugins || = [ ]
2013-08-01 01:59:57 -04:00
end
2018-05-08 01:24:58 -04:00
def self . hidden_plugins
@hidden_plugins || = [ ]
end
2018-05-08 19:52:21 -04:00
def self . visible_plugins
2018-05-08 01:24:58 -04:00
self . plugins - self . hidden_plugins
end
2017-01-12 15:43:09 -05:00
def self . plugin_themes
@plugin_themes || = plugins . map ( & :themes ) . flatten
end
2016-11-14 19:42:55 -05: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 10:52:54 -04: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 05:50:31 -04:00
next if ! args [ :include_disabled ] && ! plugin . enabled?
2019-07-15 10:52:54 -04:00
true
end
end
2021-04-23 10:24:42 -04: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 11:30:31 -04:00
end
2021-04-23 10:24:42 -04:00
end
def self . asset_filter_options ( type , request )
result = { }
return result if request . blank?
path = request . fullpath
result [ :path ] = path if 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 11:30:31 -04:00
2019-09-16 09:56:19 -04: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 23:09:10 -04:00
assets
2019-08-20 12:39:52 -04:00
end
2019-07-15 10:52:54 -04:00
def self . find_plugin_js_assets ( args )
2020-03-13 11:30:31 -04:00
plugins = self . find_plugins ( args ) . select do | plugin |
2019-07-15 10:52:54 -04:00
plugin . js_asset_exists?
2020-03-13 11:30:31 -04:00
end
2021-04-23 10:24:42 -04:00
plugins = apply_asset_filters ( plugins , :js , args [ :request ] )
2020-03-13 11:30:31 -04:00
plugins . map { | plugin | " plugins/ #{ plugin . directory_name } " }
2019-07-15 10:52:54 -04:00
end
2014-01-14 20:07:42 -05: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 22:21:00 -04:00
message = MessageBus . last_message ( channel )
2014-01-14 20:07:42 -05:00
unless message && message . data == digest
2015-05-03 22:21:00 -04:00
MessageBus . publish channel , digest
2014-01-14 20:07:42 -05:00
end
digest
end
end
2018-08-09 11:29:02 -04:00
BUILTIN_AUTH || = [
2019-03-27 09:25:04 -04: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 07:10:43 -04:00
Auth :: AuthProvider . new ( authenticator : Auth :: DiscordAuthenticator . new , icon : " fab-discord " )
2018-07-31 11:18:50 -04: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 21:04:16 -04: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 11:18:50 -04:00
auth_providers . map ( & :authenticator )
2013-08-25 21:04:16 -04:00
end
2018-07-23 11:51:57 -04:00
def self . enabled_authenticators
authenticators . select { | authenticator | authenticator . enabled? }
2013-08-01 01:59:57 -04:00
end
2013-05-30 18:41:29 -04:00
def self . cache
2019-06-12 22:58:27 -04:00
@cache || = begin
if GlobalSetting . skip_redis?
ActiveSupport :: Cache :: MemoryStore . new
else
Cache . new
end
end
2013-05-30 18:41:29 -04:00
end
2013-02-05 14:16:51 -05:00
2020-02-17 23:11:30 -05: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 14:16:51 -05:00
# Get the current base URL for the current site
def self . current_hostname
2016-06-30 10:55:01 -04:00
SiteSetting . force_hostname . presence || RailsMultisite :: ConnectionManagement . current_hostname
2013-05-30 18:41:29 -04:00
end
2020-10-09 07:51:24 -04:00
def self . base_path ( default_value = " " )
2016-06-30 10:55:01 -04:00
ActionController :: Base . config . relative_url_root . presence || default_value
2013-03-14 08:01:52 -04:00
end
2020-10-09 07:51:24 -04: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 13:54:17 -04:00
def self . base_protocol
SiteSetting . force_https? ? " https " : " http "
end
2013-05-30 18:41:29 -04:00
def self . base_url_no_prefix
2016-07-28 13:54:17 -04:00
default_port = SiteSetting . force_https? ? 443 : 80
2019-05-02 18:17:27 -04:00
url = + " #{ base_protocol } :// #{ current_hostname } "
2016-06-30 10:55:01 -04:00
url << " : #{ SiteSetting . port } " if SiteSetting . port . to_i > 0 && SiteSetting . port . to_i != default_port
2019-05-06 01:26:57 -04:00
if Rails . env . development? && SiteSetting . port . blank?
url << " : #{ ENV [ " UNICORN_PORT " ] || 3000 } "
end
2016-06-30 10:55:01 -04:00
url
2013-04-05 06:38:20 -04:00
end
2013-05-30 18:41:29 -04:00
def self . base_url
2020-10-09 07:51:24 -04:00
base_url_no_prefix + base_path
2013-05-30 18:41:29 -04:00
end
2017-07-19 15:08:54 -04:00
def self . route_for ( uri )
2018-03-28 04:20:08 -04:00
unless uri . is_a? ( URI )
uri = begin
URI ( uri )
2020-11-20 04:28:14 -05:00
rescue ArgumentError , URI :: Error
2018-03-28 04:20:08 -04:00
end
end
2017-07-19 15:08:54 -04:00
return unless uri
2019-05-02 18:17:27 -04:00
path = + ( uri . path || " " )
2020-10-09 07:51:24 -04:00
if ! uri . host || ( uri . host == Discourse . current_hostname && path . start_with? ( Discourse . base_path ) )
path . slice! ( Discourse . base_path )
2017-07-19 15:08:54 -04:00
return Rails . application . routes . recognize_path ( path )
end
2017-07-20 16:01:16 -04:00
nil
rescue ActionController :: RoutingError
2017-07-19 15:08:54 -04:00
nil
end
2018-11-06 08:17:13 -05:00
class << self
alias_method :base_url_no_path , :base_url_no_prefix
end
2020-06-11 01:45:46 -04:00
READONLY_MODE_KEY_TTL || = 60
READONLY_MODE_KEY || = 'readonly_mode'
PG_READONLY_MODE_KEY || = 'readonly_mode:postgres'
2020-07-14 04:15:58 -04:00
PG_READONLY_MODE_KEY_TTL || = 300
2020-06-11 01:45:46 -04:00
USER_READONLY_MODE_KEY || = 'readonly_mode:user'
PG_FORCE_READONLY_MODE_KEY || = 'readonly_mode:postgres_force'
2016-06-29 02:19:18 -04:00
2022-05-17 14:06:08 -04:00
# Psuedo readonly mode, where staff can still write
STAFF_WRITES_ONLY_MODE_KEY || = 'readonly_mode:staff_writes_only'
2017-01-11 05:03:36 -05:00
READONLY_KEYS || = [
2017-01-11 03:38:07 -05:00
READONLY_MODE_KEY ,
PG_READONLY_MODE_KEY ,
2020-06-11 01:45:46 -04:00
USER_READONLY_MODE_KEY ,
PG_FORCE_READONLY_MODE_KEY
2017-01-11 03:38:07 -05:00
]
def self . enable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 05:27:24 -05:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . pause! ( " pg_failover " ) if ! Sidekiq . paused?
end
2022-05-17 14:06:08 -04:00
if [ USER_READONLY_MODE_KEY , PG_FORCE_READONLY_MODE_KEY , STAFF_WRITES_ONLY_MODE_KEY ] . include? ( key )
2019-12-03 04:05:53 -05:00
Discourse . redis . set ( key , 1 )
2016-06-29 02:19:18 -04:00
else
2020-07-14 04:15:58 -04: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 02:19:18 -04:00
end
2016-06-29 01:55:17 -04:00
2015-05-03 22:21:00 -04:00
MessageBus . publish ( readonly_channel , true )
2013-02-05 14:16:51 -05:00
true
end
2020-07-14 04:15:58 -04:00
def self . keep_readonly_mode ( key , ttl : )
# extend the expiry by ttl minute every ttl/2 seconds
2019-02-19 21:01:18 -05:00
@mutex || = Mutex . new
@mutex . synchronize do
2018-06-19 03:44:08 -04:00
@dbs || = Set . new
@dbs << RailsMultisite :: ConnectionManagement . current_db
2018-06-18 22:15:29 -04:00
@threads || = { }
2018-06-19 03:44:08 -04:00
unless @threads [ key ] & . alive?
2018-06-18 22:15:29 -04:00
@threads [ key ] = Thread . new do
2019-02-19 21:01:18 -05:00
while @dbs . size > 0 do
2020-07-14 04:15:58 -04:00
sleep ttl / 2
2018-06-21 05:52:42 -04:00
2019-02-19 21:01:18 -05:00
@mutex . synchronize do
@dbs . each do | db |
RailsMultisite :: ConnectionManagement . with_connection ( db ) do
2020-07-14 04:15:58 -04:00
if ! Discourse . redis . expire ( key , ttl )
2019-02-19 21:01:18 -05:00
@dbs . delete ( db )
end
2018-06-19 03:44:08 -04:00
end
end
end
2018-06-18 22:15:29 -04:00
end
2016-11-10 10:44:51 -05:00
end
2015-02-11 15:50:17 -05:00
end
end
end
2017-01-11 03:38:07 -05:00
def self . disable_readonly_mode ( key = READONLY_MODE_KEY )
2020-11-11 05:27:24 -05:00
if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
Sidekiq . unpause! if Sidekiq . paused?
end
2019-12-03 04:05:53 -05:00
Discourse . redis . del ( key )
2015-05-03 22:21:00 -04:00
MessageBus . publish ( readonly_channel , false )
2013-02-05 14:16:51 -05:00
true
end
2020-06-11 01:45:46 -04: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 12:21:29 -04:00
def self . readonly_mode? ( keys = READONLY_KEYS )
2020-06-14 21:57:44 -04:00
recently_readonly? || Discourse . redis . exists? ( * keys )
2017-01-11 03:38:07 -05:00
end
2022-05-17 14:06:08 -04:00
def self . staff_writes_only_mode?
Discourse . redis . get ( STAFF_WRITES_ONLY_MODE_KEY ) . present?
end
2019-01-21 00:29:29 -05:00
def self . pg_readonly_mode?
2019-12-03 04:05:53 -05:00
Discourse . redis . get ( PG_READONLY_MODE_KEY ) . present?
2019-01-21 00:29:29 -05:00
end
2019-06-21 10:08:57 -04: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 03:38:07 -05:00
end
def self . recently_readonly?
2019-12-03 04:05:53 -05:00
postgres_read_only = postgres_last_read_only [ Discourse . redis . namespace ]
redis_read_only = redis_last_read_only [ Discourse . redis . namespace ]
2019-06-21 10:08:57 -04:00
( redis_read_only . present? && redis_read_only > 15 . seconds . ago ) ||
( postgres_read_only . present? && postgres_read_only > 15 . seconds . ago )
2017-01-11 03:38:07 -05:00
end
2019-06-21 10:08:57 -04:00
def self . received_postgres_readonly!
2019-12-03 04:05:53 -05:00
postgres_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2019-06-21 10:08:57 -04:00
end
2020-06-09 04:36:04 -04:00
def self . clear_postgres_readonly!
postgres_last_read_only [ Discourse . redis . namespace ] = nil
end
2019-06-21 10:08:57 -04:00
def self . received_redis_readonly!
2019-12-03 04:05:53 -05:00
redis_last_read_only [ Discourse . redis . namespace ] = Time . zone . now
2017-01-11 03:38:07 -05:00
end
2020-06-09 04:36:04 -04:00
def self . clear_redis_readonly!
redis_last_read_only [ Discourse . redis . namespace ] = nil
end
2017-01-11 03:38:07 -05:00
def self . clear_readonly!
2020-06-09 04:36:04 -04:00
clear_redis_readonly!
clear_postgres_readonly!
2019-01-21 20:51:45 -05:00
Site . clear_anon_cache!
true
2013-02-05 14:16:51 -05:00
end
2017-08-15 22:38:30 -04:00
def self . request_refresh! ( user_ids : nil )
2014-02-21 00:52:11 -05:00
# Causes refresh on next click for all clients
#
2015-05-03 22:21:00 -04:00
# This is better than `MessageBus.publish "/file-change", ["refresh"]` because
2014-02-21 00:52:11 -05:00
# it spreads the refreshes out over a time period
2017-08-15 22:38:30 -04:00
if user_ids
2017-08-16 00:06:47 -04:00
MessageBus . publish ( " /refresh_client " , 'clobber' , user_ids : user_ids )
2017-08-15 22:38:30 -04:00
else
MessageBus . publish ( '/global/asset-version' , 'clobber' )
end
2014-02-21 00:52:11 -05:00
end
2017-10-03 23:22:23 -04:00
def self . git_version
2022-01-08 17:39:46 -05:00
@git_version || = begin
git_cmd = 'git rev-parse HEAD'
self . try_git ( git_cmd , Discourse :: VERSION :: STRING )
end
2013-02-18 01:39:54 -05:00
end
2014-09-09 17:04:10 -04:00
def self . git_branch
2022-01-08 17:39:46 -05:00
@git_branch || = begin
git_cmd = 'git rev-parse --abbrev-ref HEAD'
self . try_git ( git_cmd , 'unknown' )
end
2017-08-28 12:24:56 -04:00
end
def self . full_version
2022-01-08 17:39:46 -05:00
@full_version || = begin
2022-01-09 14:25:58 -05:00
git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null'
2022-01-08 17:39:46 -05:00
self . try_git ( git_cmd , 'unknown' )
end
2017-08-28 12:24:56 -04:00
end
2019-05-17 01:42:45 -04:00
def self . last_commit_date
2022-01-08 17:39:46 -05:00
@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
2019-05-17 01:42:45 -04:00
end
2017-10-03 23:22:23 -04:00
def self . try_git ( git_cmd , default_value )
2017-08-28 12:24:56 -04:00
version_value = false
2014-09-09 17:04:10 -04:00
2017-10-03 23:22:23 -04:00
begin
version_value = ` #{ git_cmd } ` . strip
rescue
version_value = default_value
2014-09-09 17:04:10 -04:00
end
2017-08-28 12:24:56 -04:00
if version_value . empty?
version_value = default_value
end
version_value
2014-09-09 17:04:10 -04:00
end
2013-09-06 03:28:37 -04:00
# Either returns the site_contact_username user or the first admin.
def self . site_contact_user
2014-05-06 09:41:59 -04:00
user = User . find_by ( username_lower : SiteSetting . site_contact_username . downcase ) if SiteSetting . site_contact_username . present?
2015-11-24 14:37:33 -05:00
user || = ( system_user || User . admins . real . order ( :id ) . first )
2013-05-30 18:41:29 -04:00
end
2013-02-05 14:16:51 -05:00
2015-05-06 19:00:13 -04:00
SYSTEM_USER_ID || = - 1
2014-06-24 20:45:20 -04:00
2013-09-06 03:28:37 -04:00
def self . system_user
2019-10-31 11:16:26 -04:00
@system_users || = { }
current_db = RailsMultisite :: ConnectionManagement . current_db
@system_users [ current_db ] || = User . find_by ( id : SYSTEM_USER_ID )
2013-09-06 03:28:37 -04:00
end
2013-07-31 17:26:34 -04:00
def self . store
2017-10-06 01:20:01 -04:00
if SiteSetting . Upload . enable_s3_uploads
2013-07-31 17:26:34 -04:00
@s3_store_loaded || = require 'file_store/s3_store'
2013-11-05 13:04:47 -05:00
FileStore :: S3Store . new
2013-07-31 17:26:34 -04:00
else
@local_store_loaded || = require 'file_store/local_store'
2013-11-05 13:04:47 -05:00
FileStore :: LocalStore . new
2013-07-31 17:26:34 -04:00
end
end
2019-04-17 03:15:04 -04:00
def self . stats
2019-05-01 14:04:18 -04:00
PluginStore . new ( " stats " )
2019-04-17 03:15:04 -04:00
end
2013-10-09 00:10:37 -04: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 13:04:47 -05:00
def self . asset_host
Rails . configuration . action_controller . asset_host
end
2014-02-12 23:37:28 -05:00
def self . readonly_channel
2014-02-19 12:21:41 -05:00
" /site/read-only "
2013-02-05 14:16:51 -05:00
end
2014-02-12 23:37:28 -05:00
2014-03-27 22:48:14 -04:00
# all forking servers must call this
# after fork, otherwise Discourse will be
# in a bad state
def self . after_fork
2018-06-14 04:22:02 -04:00
# note: some of this reconnecting may no longer be needed per https://github.com/redis/redis-rb/pull/414
2015-05-03 22:21:00 -04:00
MessageBus . after_fork
2014-03-27 22:48:14 -04:00
SiteSetting . after_fork
2020-05-31 22:55:53 -04:00
Discourse . redis . reconnect
2014-03-27 22:48:14 -04:00
Rails . cache . reconnect
2019-11-26 20:35:14 -05:00
Discourse . cache . reconnect
2014-05-07 18:05:28 -04:00
Logster . store . redis . reconnect
2014-04-22 21:01:17 -04:00
# shuts down all connections in the pool
2020-06-11 02:09:19 -04:00
Sidekiq . redis_pool . shutdown { | conn | conn . disconnect! }
2014-04-22 21:01:17 -04:00
# re-establish
Sidekiq . redis = sidekiq_redis_config
2016-07-16 01:11:34 -04:00
# in case v8 was initialized we want to make sure it is nil
PrettyText . reset_context
2016-11-01 22:34:20 -04:00
2020-03-11 09:43:55 -04:00
DiscourseJsProcessor :: Transpiler . reset_context if defined? DiscourseJsProcessor :: Transpiler
2016-11-01 22:34:20 -04:00
JsLocaleHelper . reset_context if defined? JsLocaleHelper
2021-06-03 02:41:16 -04: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 18:05:28 -04:00
nil
2014-04-22 21:01:17 -04:00
end
2018-08-12 23:14:34 -04: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 02:33:06 -04:00
logster_env = env
2018-08-12 23:14:34 -04:00
if old_env = Thread . current [ Logster :: Logger :: LOGSTER_ENV ]
2018-08-13 02:33:06 -04: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 23:14:34 -04: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 02:33:06 -04:00
env : logster_env
2018-08-12 23:14:34 -04:00
)
end
2018-08-13 02:33:06 -04:00
if old_env
env . each do | k , v |
# do not leak state
logster_env . delete ( k )
end
end
nil
2018-08-12 23:14:34 -04:00
end
2017-12-01 00:23:21 -05: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 17:54:28 -05:00
env || = { }
env [ :current_db ] || = RailsMultisite :: ConnectionManagement . current_db
2017-12-01 00:23:21 -05:00
# logster
Rails . logger . add_with_opts (
:: Logger :: Severity :: WARN ,
2022-02-04 14:41:08 -05:00
" #{ message } : #{ e . class . name } : #{ e } " ,
2017-12-01 00:23:21 -05:00
" discourse-exception " ,
backtrace : e . backtrace . join ( " \n " ) ,
env : env
)
else
# no logster ... fallback
2020-06-10 22:49:46 -04:00
Rails . logger . warn ( " #{ message } #{ e } \n #{ e . backtrace . join ( " \n " ) } " )
2017-12-01 00:23:21 -05:00
end
rescue
STDERR . puts " Failed to report exception #{ e } #{ message } "
end
2019-01-03 12:03:01 -05:00
def self . deprecate ( warning , drop_from : nil , since : nil , raise_error : false , output_in_test : false )
2018-12-06 06:38:01 -05: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 03:50:11 -04:00
if Rails . env == " development "
STDERR . puts ( warning )
end
2019-01-03 12:03:01 -05:00
if output_in_test && Rails . env == " test "
STDERR . puts ( warning )
end
2018-06-20 03:50:11 -04:00
digest = Digest :: MD5 . hexdigest ( warning )
redis_key = " deprecate-notice- #{ digest } "
2022-01-06 11:50:18 -05:00
if Rails . logger && ! Discourse . redis . without_namespace . get ( redis_key )
2020-05-10 08:05:23 -04:00
Rails . logger . warn ( warning )
2019-06-21 10:08:57 -04:00
begin
2019-12-03 04:05:53 -05:00
Discourse . redis . without_namespace . setex ( redis_key , 3600 , " x " )
2019-06-21 10:08:57 -04:00
rescue Redis :: CommandError = > e
raise unless e . message =~ / READONLY /
end
2018-06-20 03:50:11 -04:00
end
warning
end
2020-04-30 02:48:34 -04:00
SIDEKIQ_NAMESPACE || = 'sidekiq'
2016-12-04 22:46:34 -05:00
2014-04-22 21:01:17 -04:00
def self . sidekiq_redis_config
2015-06-25 02:51:48 -04:00
conf = GlobalSetting . redis_config . dup
2016-12-04 22:46:34 -05:00
conf [ :namespace ] = SIDEKIQ_NAMESPACE
2015-06-25 02:51:48 -04:00
conf
2014-03-27 22:48:14 -04:00
end
2014-07-29 10:40:02 -04:00
def self . static_doc_topic_ids
[ SiteSetting . tos_topic_id , SiteSetting . guidelines_topic_id , SiteSetting . privacy_topic_id ]
end
2017-02-17 12:09:53 -05: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 16:32:15 -05: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 12:09:53 -05: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 06:27:35 -04:00
( ActiveRecord :: Base . connection . tables - %w[ schema_migrations versions ] ) . each do | table |
2017-02-17 12:09:53 -05:00
table . classify . constantize . reset_column_information rescue nil
end
nil
end
2017-11-15 16:39:11 -05:00
def self . running_in_rack?
ENV [ " DISCOURSE_RUNNING_IN_RACK " ] == " 1 "
end
2018-10-09 01:11:45 -04:00
def self . skip_post_deployment_migrations?
[ '1' , 'true' ] . include? ( ENV [ " SKIP_POST_DEPLOYMENT_MIGRATIONS " ] & . to_s )
end
2019-10-07 00:33:37 -04: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 06:32:13 -04: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 00:33:37 -04:00
2021-04-30 06:32:13 -04: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 00:33:37 -04:00
end
schema_cache = ActiveRecord :: Base . connection . schema_cache
2020-06-03 03:36:50 -04:00
RailsMultisite :: ConnectionManagement . safe_each_connection do
2021-06-01 02:57:24 -04:00
# load up schema cache for all multisite assuming all dbs have
# an identical schema
2019-10-07 00:33:37 -04: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 02:16:01 -04:00
JsLocaleHelper . load_translations ( SiteSetting . default_locale )
2021-06-02 01:25:12 -04:00
Site . json_for ( Guardian . new )
2021-06-03 04:14:56 -04:00
SvgSprite . preload
2021-06-07 23:15:55 -04: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 00:33:37 -04:00
end
2021-05-07 01:25:31 -04: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 02:57:24 -04:00
} ,
Thread . new {
SvgSprite . core_svgs
DEV: Allow Ember CLI assets to be used by development Rails app (#16511)
Previously, accessing the Rails app directly in development mode would give you assets from our 'legacy' Ember asset pipeline. The only way to run with Ember CLI assets was to run ember-cli as a proxy. This was quite limiting when working on things which are bypassed when using the ember-cli proxy (e.g. changes to `application.html.erb`). Also, since `ember-auto-import` introduced chunking, visiting `/theme-qunit` under Ember CLI was failing to include all necessary chunks.
This commit teaches Sprockets about our Ember CLI assets so that they can be used in development mode, and are automatically collected up under `/public/assets` during `assets:precompile`. As a bonus, this allows us to remove all the custom manifest modification from `assets:precompile`.
The key changes are:
- Introduce a shared `EmberCli.enabled?` helper
- When ember-cli is enabled, add ember-cli `/dist/assets` as the top-priority Rails asset directory
- Have ember-cli output a `chunks.json` manifest, and teach `preload_script` to read it and append the correct chunks to their associated `afterFile`
- Remove most custom ember-cli logic from the `assets:precompile` step. Instead, rely on Rails to take care of pulling the 'precompiled' assets into the `public/assets` directory. Move the 'renaming' logic to runtime, so it can be used in development mode as well.
- Remove fingerprinting from `ember-cli-build`, and allow Rails to take care of things
Long-term, we may want to replace Sprockets with the lighter-weight Propshaft. The changes made in this commit have been made with that long-term goal in mind.
tldr: when you visit the rails app directly, you'll now be served the current ember-cli assets. To keep these up-to-date make sure either `ember serve`, or `ember build --watch` is running. If you really want to load the old non-ember-cli assets, then you should start the server with `EMBER_CLI_PROD_ASSETS=0`. (the legacy asset pipeline will be removed very soon)
2022-04-21 11:26:34 -04:00
} ,
Thread . new {
EmberCli . script_chunks
2021-05-07 01:25:31 -04:00
}
] . each ( & :join )
2019-10-07 00:33:37 -04:00
ensure
@preloaded_rails = true
end
2019-12-03 04:05:53 -05:00
2022-01-08 17:39:46 -05:00
mattr_accessor :redis
2019-12-18 00:51:57 -05:00
def self . is_parallel_test?
ENV [ 'RAILS_ENV' ] == " test " && ENV [ 'TEST_ENV_NUMBER' ]
end
2021-01-28 21:14:49 -05: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 02:55:59 -04:00
def self . allow_dev_populate?
Rails . env . development? || ENV [ " ALLOW_DEV_POPULATE " ] == " 1 "
end
2022-07-22 02:46:52 -04:00
# warning: this method is very expensive and shouldn't be called in places
# where performance matters. it's meant to be called manually (e.g. in the
# rails console) when dealing with an emergency that requires invalidating
# theme cache
def self . clear_all_theme_cache!
ThemeField . force_recompilation!
Theme . all . each ( & :update_javascript_cache! )
Theme . expire_site_cache!
end
2013-02-05 14:16:51 -05:00
end