2019-05-03 06:17:27 +08:00
# frozen_string_literal: true
2013-05-31 06:41:29 +08:00
require " cache "
2017-03-17 14:21:30 +08:00
require " open3 "
2022-03-21 22:28:52 +08:00
require " plugin/instance "
require " version "
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 "
2023-08-23 02:30:33 +08:00
MAX_METADATA_FILE_SIZE = 64 . kilobytes
2013-02-06 03:16:51 +08:00
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
2023-01-12 19:12:20 +08:00
# TODO: Remove this once we drop support for Ruby 2.
EMPTY_KEYWORDS || = { }
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?
if command . present?
raise RuntimeError . new ( " Cannot pass command and block to execute_command " )
2023-01-09 20:10:19 +08:00
end
2019-11-07 23:47:16 +08:00
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
return << ~ TEXT if pretty_logs . size < max_logs_length
` ` ` text
#{pretty_logs}
` ` `
TEXT
# 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 )
2023-01-09 20:10:19 +08:00
end
2021-08-04 01:06:50 +08:00
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
2022-09-20 17:28:17 +08:00
class CommandError < RuntimeError
attr_reader :status , :stdout , :stderr
def initialize ( message , status : nil , stdout : nil , stderr : nil )
super ( message )
@status = status
@stdout = stdout
@stderr = stderr
end
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 )
if ( @init_params . keys & exec_params . keys ) . present?
raise RuntimeError . new ( " Cannot specify same parameters at block and command level " )
2023-01-09 20:10:19 +08:00
end
2019-11-07 23:47:16 +08:00
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?
2022-09-20 17:28:17 +08:00
raise CommandError . new (
" #{ caller [ 0 ] } : #{ failure_message } #{ stderr } " ,
stdout : stdout ,
stderr : stderr ,
status : status ,
)
2019-11-07 23:47:16 +08:00
end
stdout
end
end
2017-03-17 14:21:30 +08:00
end
2022-08-03 10:53:26 +08: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!
2023-01-16 06:08:44 +08:00
if Rails . env . test?
def self . catch_job_exceptions!
raise " tests only " if ! Rails . env . test?
@catch_job_exceptions = true
end
def self . reset_catch_job_exceptions!
raise " tests only " if ! Rails . env . test?
remove_instance_variable ( :@catch_job_exceptions )
end
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 || = { }
2022-06-21 09:23:36 +08:00
parent_logger || = Sidekiq
2014-02-21 11:30:25 +08:00
2022-08-03 12:28:46 +08:00
job = context [ :job ]
2022-08-05 15:40:22 +08:00
# mini_scheduler direct reporting
2022-08-03 12:28:46 +08:00
if Hash === job
job_class = job [ " class " ]
job_exception_stats [ job_class ] += 1 if job_class
2022-08-03 10:53:26 +08:00
end
2022-08-05 15:40:22 +08:00
# internal reporting
job_exception_stats [ job ] += 1 if job . class == Class && :: Jobs :: Base > job
2014-02-21 11:30:25 +08:00
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
2023-01-16 06:08:44 +08:00
raise ex if Rails . env . test? && ! @catch_job_exceptions
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
2024-01-17 10:01:04 +08:00
@filters || = % i [ latest unread new unseen top read posted bookmarks hot ]
2013-12-24 07:50:36 +08:00
end
def self . anonymous_filters
2024-01-17 10:01:04 +08:00
@anonymous_filters || = % i [ latest top categories hot ]
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 + % i [ categories top ]
2013-12-24 07:50:36 +08:00
end
2023-06-01 08:00:01 +08:00
# list of pixel ratios Discourse tries to optimize for
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
2023-06-01 08:00:01 +08:00
Set . new ( SiteSetting . avatar_sizes . split ( " | " ) . map ( & :to_i ) )
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 = [ ]
2023-05-10 21:21:48 +08:00
@plugins_by_name = { }
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
2023-05-10 21:21:48 +08:00
@plugins_by_name [ p . name ] = p
# The plugin directory name and metadata name should match, but that
# is not always the case
dir_name = p . path . split ( " / " ) [ - 2 ]
if p . name != dir_name
STDERR . puts " Plugin name is ' #{ p . name } ', but plugin directory is named ' #{ dir_name } ' "
# Plugins are looked up by directory name in SiteSettingExtension
# because SiteSetting.load_settings uses directory name as plugin
# name. We alias the two names just to make sure the look up works
@plugins_by_name [ dir_name ] = p
end
2015-04-28 01:06:53 +08:00
else
STDERR . puts " Could not activate #{ p . metadata . name } , discourse does not meet required version ( #{ v } ) "
2023-01-09 20:10:19 +08:00
end
2015-04-28 01:06:53 +08:00
end
2020-01-11 04:06:15 +08:00
DiscourseEvent . trigger ( :after_plugin_activation )
2013-08-01 13:59:57 +08:00
end
def self . plugins
2015-02-11 00:18:16 +08:00
@plugins || = [ ]
2013-08-01 13:59:57 +08:00
end
2023-05-10 21:21:48 +08:00
def self . plugins_by_name
@plugins_by_name || = { }
2018-05-08 13:24:58 +08:00
end
2018-05-09 07:52:21 +08:00
def self . visible_plugins
2023-05-10 21:21:48 +08:00
plugins . filter ( & :visible? )
2018-05-08 13:24:58 +08:00
end
2023-12-21 09:37:20 +08:00
def self . plugins_sorted_by_name ( enabled_only : true )
2024-01-08 07:57:25 +08:00
if enabled_only
return visible_plugins . filter ( & :enabled? ) . sort_by { | plugin | plugin . humanized_name . downcase }
end
visible_plugins . sort_by { | plugin | plugin . humanized_name . downcase }
2023-12-21 09:37:20 +08:00
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 { | plugin | plugin . asset_filters . all? { | b | b . call ( type , request , filter_opts ) } }
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 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 { | plugin | plugin . css_asset_exists? ( target ) }
. map do | plugin |
target . nil? ? plugin . directory_name : " #{ plugin . directory_name } _ #{ target } "
end
end
2019-08-22 11:09:10 +08:00
2023-06-01 10:27:11 +08:00
assets . map! { | asset | " #{ asset } _rtl " } if args [ :rtl ]
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 |
2022-08-25 18:36:02 +08:00
plugin . js_asset_exists? || plugin . extra_js_asset_exists? || plugin . admin_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
2022-08-22 16:56:39 +08:00
plugins . flat_map do | plugin |
assets = [ ]
assets << " plugins/ #{ plugin . directory_name } " if plugin . js_asset_exists?
assets << " plugins/ #{ plugin . directory_name } _extra " if plugin . extra_js_asset_exists?
2022-08-25 18:36:02 +08:00
# TODO: make admin asset only load for admins
assets << " plugins/ #{ plugin . directory_name } _admin " if plugin . admin_js_asset_exists?
2022-08-22 16:56:39 +08:00
assets
end
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
MessageBus . publish channel , digest unless message && message . data == digest
2015-05-04 10:21:00 +08:00
digest
2014-01-15 09:07:42 +08:00
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 " ) ,
2024-04-19 18:47:30 +08:00
Auth :: AuthProvider . new (
authenticator : Auth :: LinkedInOidcAuthenticator . new ,
icon : " fab-linkedin-in " ,
) ,
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
2023-01-09 20:10:19 +08:00
end
2019-06-13 10:58:27 +08:00
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
2022-12-22 21:13:43 +08:00
def self . current_hostname_with_port
2016-07-29 01:54:17 +08:00
default_port = SiteSetting . force_https? ? 443 : 80
2022-12-22 21:13:43 +08:00
result = + " #{ current_hostname } "
if SiteSetting . port . to_i > 0 && SiteSetting . port . to_i != default_port
result << " : #{ SiteSetting . port } "
2019-05-06 13:26:57 +08:00
end
result << " : #{ ENV [ " UNICORN_PORT " ] || 3000 } " if Rails . env . development? && SiteSetting . port . blank?
2023-01-09 20:10:19 +08:00
2022-12-22 21:13:43 +08:00
result
end
def self . base_url_no_prefix
" #{ base_protocol } :// #{ current_hostname_with_port } "
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
2023-06-08 02:31:20 +08:00
def self . urls_cache
@urls_cache || = DistributedCache . new ( " urls_cache " )
end
def self . tos_url
if SiteSetting . tos_url . present?
SiteSetting . tos_url
else
2023-11-09 08:33:38 +08:00
return urls_cache [ " tos " ] if urls_cache [ " tos " ] . present?
tos_url =
2023-06-08 02:31:20 +08:00
if SiteSetting . tos_topic_id > 0 && Topic . exists? ( id : SiteSetting . tos_topic_id )
" #{ Discourse . base_path } /tos "
end
2023-11-09 08:33:38 +08:00
if tos_url
urls_cache [ " tos " ] = tos_url
else
urls_cache . delete ( " tos " )
end
2023-06-08 02:31:20 +08:00
end
end
def self . privacy_policy_url
if SiteSetting . privacy_policy_url . present?
SiteSetting . privacy_policy_url
else
2023-11-09 08:33:38 +08:00
return urls_cache [ " privacy_policy " ] if urls_cache [ " privacy_policy " ] . present?
privacy_policy_url =
2023-06-08 02:31:20 +08:00
if SiteSetting . privacy_topic_id > 0 && Topic . exists? ( id : SiteSetting . privacy_topic_id )
" #{ Discourse . base_path } /privacy "
end
2023-11-09 08:33:38 +08:00
if privacy_policy_url
urls_cache [ " privacy_policy " ] = privacy_policy_url
else
urls_cache . delete ( " privacy_policy " )
end
2023-06-08 02:31:20 +08:00
end
end
def self . clear_urls!
urls_cache . clear
end
2023-03-30 22:14:59 +08:00
LAST_POSTGRES_READONLY_KEY = " postgres:last_readonly "
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
2023-04-04 01:27:32 +08:00
# Pseudo readonly mode, where staff can still write
2022-05-18 02:06:08 +08:00
STAFF_WRITES_ONLY_MODE_KEY || = " readonly_mode:staff_writes_only "
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
2022-05-18 02:06:08 +08:00
if [ USER_READONLY_MODE_KEY , PG_FORCE_READONLY_MODE_KEY , STAFF_WRITES_ONLY_MODE_KEY ] . include? (
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
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
@dbs . delete ( db ) if ! Discourse . redis . expire ( key , ttl )
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 )
2023-01-19 21:59:11 +08:00
recently_readonly? || GlobalSetting . pg_force_readonly_mode || Discourse . redis . exists? ( * keys )
2017-01-11 16:38:07 +08:00
end
2022-05-18 02:06:08 +08:00
def self . staff_writes_only_mode?
Discourse . redis . get ( STAFF_WRITES_ONLY_MODE_KEY ) . present?
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
2023-03-30 22:14:59 +08:00
@postgres_last_read_only || = DistributedCache . new ( " postgres_last_read_only " )
2019-06-21 22:08:57 +08:00
end
# Per-process
def self . redis_last_read_only
@redis_last_read_only || = { }
2017-01-11 16:38:07 +08:00
end
2023-03-30 22:14:59 +08:00
def self . postgres_recently_readonly?
2023-04-11 02:54:55 +08:00
seconds =
postgres_last_read_only . defer_get_set ( " timestamp " ) { redis . get ( LAST_POSTGRES_READONLY_KEY ) }
2023-03-30 22:14:59 +08:00
2023-04-11 02:54:55 +08:00
seconds ? Time . zone . at ( seconds . to_i ) > 15 . seconds . ago : false
2023-03-30 22:14:59 +08:00
end
2017-01-11 16:38:07 +08:00
def self . recently_readonly?
2019-12-03 17:05:53 +08:00
redis_read_only = redis_last_read_only [ Discourse . redis . namespace ]
2019-06-21 22:08:57 +08:00
2023-03-30 22:14:59 +08:00
( redis_read_only . present? && redis_read_only > 15 . seconds . ago ) || postgres_recently_readonly?
2017-01-11 16:38:07 +08:00
end
2019-06-21 22:08:57 +08:00
def self . received_postgres_readonly!
2023-03-30 22:14:59 +08:00
time = Time . zone . now
redis . set ( LAST_POSTGRES_READONLY_KEY , time . to_i . to_s )
2023-07-12 22:49:28 +08:00
postgres_last_read_only . clear ( after_commit : false )
2023-03-30 22:14:59 +08:00
time
2019-06-21 22:08:57 +08:00
end
2020-06-09 16:36:04 +08:00
def self . clear_postgres_readonly!
2023-03-30 22:14:59 +08:00
redis . del ( LAST_POSTGRES_READONLY_KEY )
2023-07-12 22:49:28 +08:00
postgres_last_read_only . clear ( after_commit : false )
2020-06-09 16:36:04 +08:00
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 . git_version
2022-01-09 06:39:46 +08:00
@git_version || =
begin
git_cmd = " git rev-parse HEAD "
self . try_git ( git_cmd , Discourse :: VERSION :: STRING )
end
2013-02-18 14:39:54 +08:00
end
2014-09-10 05:04:10 +08:00
def self . git_branch
2022-01-09 06:39:46 +08:00
@git_branch || =
2023-04-18 18:51:54 +08:00
self . try_git ( " git branch --show-current " , nil ) ||
self . try_git ( " git config user.discourse-version " , " unknown " )
2017-08-29 00:24:56 +08:00
end
def self . full_version
2022-01-09 06:39:46 +08:00
@full_version || =
begin
2022-01-10 03:25:58 +08:00
git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null'
2022-01-09 06:39:46 +08:00
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
2022-01-09 06:39:46 +08: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 13:42:45 +08:00
end
2017-10-04 11:22:23 +08:00
def self . try_git ( git_cmd , default_value )
begin
2023-04-18 16:58:57 +08:00
` #{ git_cmd } ` . strip
2017-10-04 11:22:23 +08:00
rescue StandardError
2023-04-18 16:58:57 +08:00
default_value
end . presence || default_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
2024-05-29 16:13:23 +08:00
Rails . logger . error ( " Failed to warm up pretty text: #{ e } \n #{ e . backtrace . join ( " \n " ) } " )
2021-06-03 14:41:16 +08:00
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 ]
loggers . concat ( Rails . logger . chained ) if Rails . logger . chained
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 { | k , v | logster_env [ k ] = v }
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 , env : logster_env )
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 ,
2022-02-05 03:41:08 +08:00
" #{ message } : #{ e . class . name } : #{ e } " ,
2017-12-01 13:23:21 +08:00
" 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 StandardError
STDERR . puts " Failed to report exception #{ e } #{ message } "
end
2023-01-10 01:56:43 +08:00
def self . capture_exceptions ( message : " " , env : nil )
yield
rescue Exception = > e
Discourse . warn_exception ( e , message : message , env : env )
nil
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 ( " " )
raise Deprecation . new ( warning ) if raise_error
2023-05-25 20:51:38 +08:00
STDERR . puts ( warning ) if Rails . env . development?
2018-06-20 15:50:11 +08:00
2023-05-25 20:51:38 +08:00
STDERR . puts ( warning ) if output_in_test && Rails . env . test?
2019-01-04 01:03:01 +08:00
2018-06-20 15:50:11 +08:00
digest = Digest :: MD5 . hexdigest ( warning )
redis_key = " deprecate-notice- #{ digest } "
2023-05-25 20:51:38 +08:00
if ! Rails . env . development? && Rails . logger && ! GlobalSetting . skip_redis? &&
2023-04-24 19:17:51 +08:00
! 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 |
2023-01-09 20:10:19 +08:00
begin
2017-02-18 01:09:53 +08:00
table . classify . constantize . reset_column_information
rescue StandardError
nil
2023-01-09 20:10:19 +08:00
end
2017-02-18 01:09:53 +08:00
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?
%w[ 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 |
2023-01-09 20:10:19 +08:00
begin
2021-04-30 18:32:13 +08:00
table . classify . constantize . first
rescue StandardError
nil
2023-01-09 20:10:19 +08:00
end
2021-04-30 18:32:13 +08:00
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 do
# router warm up
2023-01-09 20:10:19 +08:00
begin
2021-05-07 13:25:31 +08:00
Rails . application . routes . recognize_path ( " abc " )
rescue StandardError
nil
2023-01-09 20:10:19 +08:00
end
2021-05-07 13:25:31 +08:00
end ,
Thread . new do
# preload discourse version
Discourse . git_version
Discourse . git_branch
Discourse . full_version
2023-06-26 12:39:57 +08:00
Discourse . plugins . each { | p | p . commit_url }
2021-05-07 13:25:31 +08:00
end ,
Thread . new do
require " actionview_precompiler "
ActionviewPrecompiler . precompile
end ,
Thread . new { LetterAvatar . image_magick_version } ,
2021-06-01 14:57:24 +08: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 23:26:34 +08:00
Thread . new { EmberCli . script_chunks } ,
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
2022-01-09 06:39:46 +08:00
mattr_accessor :redis
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 || = %w[ GET HEAD OPTIONS ]
def self . is_cdn_request? ( env , request_method )
2024-05-27 18:27:13 +08:00
return if CDN_REQUEST_METHODS . exclude? ( request_method )
2021-01-29 10:14:49 +08:00
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
2022-07-22 14:46:52 +08: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
2022-09-27 16:56:06 +08:00
def self . anonymous_locale ( request )
locale =
HttpLanguageParser . parse ( request . cookies [ " locale " ] ) if SiteSetting . set_locale_from_cookie
locale || =
HttpLanguageParser . parse (
request . env [ " HTTP_ACCEPT_LANGUAGE " ] ,
) if SiteSetting . set_locale_from_accept_language_header
locale
end
2013-02-06 03:16:51 +08:00
end