2013-05-30 18:41:29 -04:00
require 'cache'
2017-03-17 02:21:30 -04:00
require 'open3'
2017-12-21 16:29:11 -05:00
require_dependency 'route_format'
2013-08-23 02:21:52 -04:00
require_dependency 'plugin/instance'
2013-10-09 00:10:37 -04:00
require_dependency 'auth/default_current_user_provider'
2015-04-27 13:06:53 -04:00
require_dependency 'version'
2017-09-08 13:38:46 -04:00
require 'digest/sha1'
2013-05-30 18:41:29 -04:00
2015-07-14 14:52:35 -04:00
# Prevents errors with reloading dev with conditional includes
if Rails . env . development?
require_dependency 'file_store/s3_store'
require_dependency 'file_store/local_store'
end
2013-02-05 14:16:51 -05:00
module Discourse
2014-04-17 01:57:17 -04:00
require 'sidekiq/exception_handler'
2014-02-20 22:30:25 -05:00
class SidekiqExceptionHandler
extend Sidekiq :: ExceptionHandler
end
2017-03-17 02:21:30 -04:00
class Utils
def self . execute_command ( * command , failure_message : " " )
stdout , stderr , status = Open3 . capture3 ( * command )
if ! status . success?
failure_message = " #{ failure_message } \n " if ! failure_message . blank?
raise " #{ failure_message } #{ stderr } "
end
stdout
end
def self . pretty_logs ( logs )
logs . join ( " \n " . freeze )
end
end
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 )
2014-02-20 22:30:25 -05: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 ) )
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
2017-09-23 10:39:58 -04:00
attr_reader :obj , :custom_message
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
opts || = { }
@custom_message = opts [ :custom_message ] if opts [ :custom_message ]
2015-09-18 03:14:10 -04:00
@obj = obj
end
end
2013-02-05 14:16:51 -05:00
# When something they want is not found
2017-09-26 12:58:15 -04:00
class NotFound < StandardError ; 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
2013-12-23 18:50:36 -05:00
def self . filters
2015-07-27 02:46:50 -04:00
@filters || = [ :latest , :unread , :new , :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
2014-01-14 12:48:57 -05:00
@top_menu_items || = Discourse . filters + [ :category , :categories , :top ]
2013-12-23 18:50:36 -05:00
end
def self . anonymous_top_menu_items
2014-01-14 12:48:57 -05:00
@anonymous_top_menu_items || = Discourse . anonymous_filters + [ :category , :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 |
set << size * pixel_ratio
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
all_plugins = Plugin :: Instance . find_all ( " #{ Rails . root } /plugins " )
2017-09-08 13:38:46 -04:00
if Rails . env . development?
plugin_hash = Digest :: SHA1 . hexdigest ( all_plugins . map { | p | p . path } . sort . join ( '|' ) )
hash_file = " #{ Rails . root } /tmp/plugin-hash "
old_hash = File . read ( hash_file ) rescue nil
if old_hash && old_hash != plugin_hash
puts " WARNING: It looks like your discourse plugins have recently changed. "
puts " It is highly recommended to remove your `tmp` directory, otherwise "
puts " plugins might not work. "
puts
else
File . write ( hash_file , plugin_hash )
end
end
2015-04-27 13:06:53 -04:00
@plugins = [ ]
all_plugins . each do | p |
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
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
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
2017-07-27 21:20:09 -04:00
plugins . find_all { | p | p . metadata . official? }
2016-11-14 19:42:55 -05:00
end
def self . unofficial_plugins
2017-07-27 21:20:09 -04:00
plugins . find_all { | p | ! p . metadata . official? }
2016-11-14 19:42:55 -05: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
2013-08-25 21:04:16 -04:00
def self . authenticators
# TODO: perhaps we don't need auth providers and authenticators maybe one object is enough
# NOTE: this bypasses the site settings and gives a list of everything, we need to register every middleware
# for the cases of multisite
# In future we may change it so we don't include them all for cases where we are not a multisite, but we would
# require a restart after site settings change
Users :: OmniauthCallbacksController :: BUILTIN_AUTH + auth_providers . map ( & :authenticator )
end
2013-08-01 01:59:57 -04:00
def self . auth_providers
2013-08-01 02:05:46 -04:00
providers = [ ]
2015-02-10 11:18:16 -05:00
plugins . each do | p |
next unless p . auth_providers
p . auth_providers . each do | prov |
providers << prov
2013-08-01 01:59:57 -04:00
end
end
providers
end
2013-05-30 18:41:29 -04:00
def self . cache
@cache || = Cache . new
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
2013-11-05 13:04:47 -05:00
def self . base_uri ( 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
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
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
url
2013-04-05 06:38:20 -04:00
end
2013-05-30 18:41:29 -04:00
def self . base_url
2015-09-21 14:28:20 -04:00
base_url_no_prefix + base_uri
2013-05-30 18:41:29 -04:00
end
2017-07-19 15:08:54 -04:00
def self . route_for ( uri )
uri = URI ( uri ) rescue nil unless ( uri . is_a? ( URI ) )
return unless uri
path = uri . path || " "
if ( uri . host == Discourse . current_hostname &&
path . start_with? ( Discourse . base_uri ) ) ||
! uri . host
path . slice! ( Discourse . base_uri )
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
2017-01-11 05:03:36 -05:00
READONLY_MODE_KEY_TTL || = 60
READONLY_MODE_KEY || = 'readonly_mode' . freeze
PG_READONLY_MODE_KEY || = 'readonly_mode:postgres' . freeze
2016-06-29 02:19:18 -04:00
USER_READONLY_MODE_KEY || = 'readonly_mode:user' . freeze
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 ,
USER_READONLY_MODE_KEY
]
def self . enable_readonly_mode ( key = READONLY_MODE_KEY )
if key == USER_READONLY_MODE_KEY
$redis . set ( key , 1 )
2016-06-29 02:19:18 -04:00
else
2017-01-11 03:38:07 -05:00
$redis . setex ( key , READONLY_MODE_KEY_TTL , 1 )
keep_readonly_mode ( key )
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
2017-01-11 03:38:07 -05:00
def self . keep_readonly_mode ( key )
2015-02-11 15:50:17 -05:00
# extend the expiry by 1 minute every 30 seconds
2016-11-10 10:44:51 -05:00
unless Rails . env . test?
Thread . new do
while readonly_mode?
2017-01-11 03:38:07 -05:00
$redis . expire ( key , READONLY_MODE_KEY_TTL )
2016-11-10 10:44:51 -05:00
sleep 30 . seconds
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 )
2016-06-29 02:19:18 -04:00
$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
2014-02-12 23:37:28 -05:00
def self . readonly_mode?
2017-09-07 01:29:30 -04:00
recently_readonly? || $redis . mget ( * READONLY_KEYS ) . compact . present?
2017-01-11 03:38:07 -05:00
end
def self . last_read_only
@last_read_only || = { }
end
def self . recently_readonly?
2017-01-11 05:03:36 -05:00
return false unless read_only = last_read_only [ $redis . namespace ]
2017-01-11 03:38:07 -05:00
read_only > 15 . seconds . ago
end
def self . received_readonly!
last_read_only [ $redis . namespace ] = Time . zone . now
end
def self . clear_readonly!
last_read_only [ $redis . namespace ] = nil
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 . 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 17:25:57 -04:00
2017-10-03 23:22:23 -04: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 )
end
2013-02-18 01:39:54 -05:00
end
2014-09-09 17:04:10 -04:00
def self . git_branch
2017-10-03 23:22:23 -04: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 12:24:56 -04:00
end
def self . full_version
2017-10-03 23:22:23 -04: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 12:24:56 -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
2016-04-25 17:03:17 -04:00
@system_user || = 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
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
2015-05-05 19:53:10 -04:00
# note: all this reconnecting may no longer be needed per https://github.com/redis/redis-rb/pull/414
2014-04-07 13:38:47 -04:00
current_db = RailsMultisite :: ConnectionManagement . current_db
RailsMultisite :: ConnectionManagement . establish_connection ( db : current_db )
2015-05-03 22:21:00 -04:00
MessageBus . after_fork
2014-03-27 22:48:14 -04:00
SiteSetting . after_fork
$redis . client . reconnect
Rails . 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
2017-07-27 21:20:09 -04:00
Sidekiq . redis_pool . shutdown { | c | nil }
2014-04-22 21:01:17 -04:00
# re-establish
Sidekiq . redis = sidekiq_redis_config
2014-08-11 03:51:55 -04:00
start_connection_reaper
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
2016-11-02 01:59:58 -04:00
Tilt :: ES6ModuleTranspilerTemplate . reset_context if defined? Tilt :: ES6ModuleTranspilerTemplate
2016-11-01 22:34:20 -04:00
JsLocaleHelper . reset_context if defined? JsLocaleHelper
2014-05-07 18:05:28 -04:00
nil
2014-04-22 21:01:17 -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 ,
" #{ message } : #{ e } " ,
" discourse-exception " ,
backtrace : e . backtrace . join ( " \n " ) ,
env : env
)
else
# no logster ... fallback
Rails . logger . warn ( " #{ message } #{ e } " )
end
rescue
STDERR . puts " Failed to report exception #{ e } #{ message } "
end
2015-02-16 17:58:23 -05:00
def self . start_connection_reaper
return if GlobalSetting . connection_reaper_age < 1 ||
GlobalSetting . connection_reaper_interval < 1
2014-08-11 03:51:55 -04:00
# this helps keep connection counts in check
Thread . new do
while true
2015-02-16 17:58:23 -05:00
begin
sleep GlobalSetting . connection_reaper_interval
2015-10-16 20:29:16 -04:00
reap_connections ( GlobalSetting . connection_reaper_age , GlobalSetting . connection_reaper_max_age )
2015-02-16 17:58:23 -05:00
rescue = > e
2017-12-01 00:23:21 -05:00
Discourse . warn_exception ( e , message : " Error reaping connections " )
2014-08-11 03:51:55 -04:00
end
end
end
end
2015-10-16 20:29:16 -04:00
def self . reap_connections ( idle , max_age )
2015-02-16 17:58:23 -05:00
pools = [ ]
2017-07-27 21:20:09 -04:00
ObjectSpace . each_object ( ActiveRecord :: ConnectionAdapters :: ConnectionPool ) { | pool | pools << pool }
2015-02-16 17:58:23 -05:00
pools . each do | pool |
2015-10-16 20:29:16 -04:00
pool . drain ( idle . seconds , max_age . seconds )
2015-02-16 17:58:23 -05:00
end
end
2016-12-04 22:46:34 -05:00
SIDEKIQ_NAMESPACE || = 'sidekiq' . freeze
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
2017-07-27 21:20:09 -04:00
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
2013-02-05 14:16:51 -05:00
end