Module: Discourse

Defined in:
lib/version.rb,
lib/discourse.rb

Defined Under Namespace

Modules: VERSION Classes: CSRF, Deprecation, ImageMagickMissing, InvalidAccess, InvalidMigration, InvalidParameters, InvalidVersionListError, NotFound, NotLoggedIn, ReadOnly, ScssError, SiteSettingMissing, TooManyMatches, Utils

Constant Summary collapse

MAX_METADATA_FILE_SIZE =
64.kilobytes
LAST_POSTGRES_READONLY_KEY =
"postgres:last_readonly"

Class Method Summary collapse

Class Method Details

.activate_plugins!Object



339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/discourse.rb', line 339

def self.activate_plugins!
  @plugins = []
  @plugins_by_name = {}
  Plugin::Instance
    .find_all("#{Rails.root}/plugins")
    .each do |p|
      v = p..required_version || Discourse::VERSION::STRING
      if Discourse.has_needed_version?(Discourse::VERSION::STRING, v)
        p.activate!
        @plugins << p
        @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
      else
        STDERR.puts "Could not activate #{p..name}, discourse does not meet required version (#{v})"
      end
    end
  DiscourseEvent.trigger(:after_plugin_activation)
end

.after_forkObject

all forking servers must call this after fork, otherwise Discourse will be in a bad state



903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
# File 'lib/discourse.rb', line 903

def self.after_fork
  # note: some of this reconnecting may no longer be needed per https://github.com/redis/redis-rb/pull/414
  MessageBus.after_fork
  SiteSetting.after_fork
  Discourse.redis.reconnect
  Rails.cache.reconnect
  Discourse.cache.reconnect
  Logster.store.redis.reconnect
  # shuts down all connections in the pool
  Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! }
  # re-establish
  Sidekiq.redis = sidekiq_redis_config

  # in case v8 was initialized we want to make sure it is nil
  PrettyText.reset_context

  DiscourseJsProcessor::Transpiler.reset_context if defined?(DiscourseJsProcessor::Transpiler)
  JsLocaleHelper.reset_context if defined?(JsLocaleHelper)

  # 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

  nil
end

.allow_dev_populate?Boolean

Returns:

  • (Boolean)


1183
1184
1185
# File 'lib/discourse.rb', line 1183

def self.allow_dev_populate?
  Rails.env.development? || ENV["ALLOW_DEV_POPULATE"] == "1"
end

.anonymous_filtersObject



319
320
321
# File 'lib/discourse.rb', line 319

def self.anonymous_filters
  @anonymous_filters ||= %i[latest top categories]
end

.anonymous_locale(request) ⇒ Object



1197
1198
1199
1200
1201
1202
1203
1204
1205
# File 'lib/discourse.rb', line 1197

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

.anonymous_top_menu_itemsObject



327
328
329
# File 'lib/discourse.rb', line 327

def self.anonymous_top_menu_items
  @anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top]
end

.apply_asset_filters(plugins, type, request) ⇒ Object



402
403
404
405
# File 'lib/discourse.rb', line 402

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

.apply_cdn_headers(headers) ⇒ Object



1177
1178
1179
1180
1181
# File 'lib/discourse.rb', line 1177

def self.apply_cdn_headers(headers)
  headers["Access-Control-Allow-Origin"] = "*"
  headers["Access-Control-Allow-Methods"] = CDN_REQUEST_METHODS.join(", ")
  headers
end

.asset_filter_options(type, request) ⇒ Object



407
408
409
410
411
412
413
414
415
# File 'lib/discourse.rb', line 407

def self.asset_filter_options(type, request)
  result = {}
  return result if request.blank?

  path = request.fullpath
  result[:path] = path if path.present?

  result
end

.asset_hostObject



892
893
894
# File 'lib/discourse.rb', line 892

def self.asset_host
  Rails.configuration.action_controller.asset_host
end

.assets_digestObject



459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/discourse.rb', line 459

def self.assets_digest
  @assets_digest ||=
    begin
      digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join)

      channel = "/global/asset-version"
      message = MessageBus.last_message(channel)

      MessageBus.publish channel, digest unless message && message.data == digest
      digest
    end
end

.auth_providersObject



489
490
491
# File 'lib/discourse.rb', line 489

def self.auth_providers
  BUILTIN_AUTH + DiscoursePluginRegistry.auth_providers.to_a
end

.authenticatorsObject



497
498
499
500
501
# File 'lib/discourse.rb', line 497

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
  auth_providers.map(&:authenticator)
end

.avatar_sizesObject



334
335
336
337
# File 'lib/discourse.rb', line 334

def self.avatar_sizes
  # TODO: should cache these when we get a notification system for site settings
  Set.new(SiteSetting.avatar_sizes.split("|").map(&:to_i))
end

.base_path(default_value = "") ⇒ Object



541
542
543
# File 'lib/discourse.rb', line 541

def self.base_path(default_value = "")
  ActionController::Base.config.relative_url_root.presence || default_value
end

.base_protocolObject



550
551
552
# File 'lib/discourse.rb', line 550

def self.base_protocol
  SiteSetting.force_https? ? "https" : "http"
end

.base_uri(default_value = "") ⇒ Object



545
546
547
548
# File 'lib/discourse.rb', line 545

def self.base_uri(default_value = "")
  deprecate("Discourse.base_uri is deprecated, use Discourse.base_path instead")
  base_path(default_value)
end

.base_urlObject



570
571
572
# File 'lib/discourse.rb', line 570

def self.base_url
  base_url_no_prefix + base_path
end

.base_url_no_prefixObject Also known as: base_url_no_path



566
567
568
# File 'lib/discourse.rb', line 566

def self.base_url_no_prefix
  "#{base_protocol}://#{current_hostname_with_port}"
end

.cacheObject



507
508
509
510
511
512
513
514
515
516
# File 'lib/discourse.rb', line 507

def self.cache
  @cache ||=
    begin
      if GlobalSetting.skip_redis?
        ActiveSupport::Cache::MemoryStore.new
      else
        Cache.new
      end
    end
end

.capture_exceptions(message: "", env: nil) ⇒ Object



998
999
1000
1001
1002
1003
# File 'lib/discourse.rb', line 998

def self.capture_exceptions(message: "", env: nil)
  yield
rescue Exception => e
  Discourse.warn_exception(e, message: message, env: env)
  nil
end

.catch_job_exceptions!Object



196
197
198
199
# File 'lib/discourse.rb', line 196

def self.catch_job_exceptions!
  raise "tests only" if !Rails.env.test?
  @catch_job_exceptions = true
end

.clear_all_theme_cache!Object

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



1191
1192
1193
1194
1195
# File 'lib/discourse.rb', line 1191

def self.clear_all_theme_cache!
  ThemeField.force_recompilation!
  Theme.all.each(&:update_javascript_cache!)
  Theme.expire_site_cache!
end

.clear_postgres_readonly!Object



782
783
784
785
# File 'lib/discourse.rb', line 782

def self.clear_postgres_readonly!
  redis.del(LAST_POSTGRES_READONLY_KEY)
  postgres_last_read_only.clear(after_commit: false)
end

.clear_readonly!Object



795
796
797
798
799
800
# File 'lib/discourse.rb', line 795

def self.clear_readonly!
  clear_redis_readonly!
  clear_postgres_readonly!
  Site.clear_anon_cache!
  true
end

.clear_redis_readonly!Object



791
792
793
# File 'lib/discourse.rb', line 791

def self.clear_redis_readonly!
  redis_last_read_only[Discourse.redis.namespace] = nil
end

.clear_urls!Object



637
638
639
# File 'lib/discourse.rb', line 637

def self.clear_urls!
  urls_cache.clear
end

.current_hostnameObject

Get the current base URL for the current site



537
538
539
# File 'lib/discourse.rb', line 537

def self.current_hostname
  SiteSetting.force_hostname.presence || RailsMultisite::ConnectionManagement.current_hostname
end

.current_hostname_with_portObject



554
555
556
557
558
559
560
561
562
563
564
# File 'lib/discourse.rb', line 554

def self.current_hostname_with_port
  default_port = SiteSetting.force_https? ? 443 : 80
  result = +"#{current_hostname}"
  if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port
    result << ":#{SiteSetting.port}"
  end

  result << ":#{ENV["UNICORN_PORT"] || 3000}" if Rails.env.development? && SiteSetting.port.blank?

  result
end

.current_user_providerObject



884
885
886
# File 'lib/discourse.rb', line 884

def self.current_user_provider
  @current_user_provider || Auth::DefaultCurrentUserProvider
end

.current_user_provider=(val) ⇒ Object



888
889
890
# File 'lib/discourse.rb', line 888

def self.current_user_provider=(val)
  @current_user_provider = val
end

.deprecate(warning, drop_from: nil, since: nil, raise_error: false, output_in_test: false) ⇒ Object

Raises:



1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
# File 'lib/discourse.rb', line 1005

def self.deprecate(warning, drop_from: nil, since: nil, raise_error: false, output_in_test: false)
  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 << "\nAt #{location}"
  warning = warning.join(" ")

  raise Deprecation.new(warning) if raise_error

  STDERR.puts(warning) if Rails.env.development?

  STDERR.puts(warning) if output_in_test && Rails.env.test?

  digest = Digest::MD5.hexdigest(warning)
  redis_key = "deprecate-notice-#{digest}"

  if !Rails.env.development? && Rails.logger && !GlobalSetting.skip_redis? &&
       !Discourse.redis.without_namespace.get(redis_key)
    Rails.logger.warn(warning)
    begin
      Discourse.redis.without_namespace.setex(redis_key, 3600, "x")
    rescue Redis::CommandError => e
      raise unless e.message =~ /READONLY/
    end
  end
  warning
end

.disable_pg_force_readonly_modeObject



731
732
733
734
735
736
737
# File 'lib/discourse.rb', line 731

def self.disable_pg_force_readonly_mode
  RailsMultisite::ConnectionManagement.each_connection do
    disable_readonly_mode(PG_FORCE_READONLY_MODE_KEY)
  end

  true
end

.disable_readonly_mode(key = READONLY_MODE_KEY) ⇒ Object



713
714
715
716
717
718
719
720
721
# File 'lib/discourse.rb', line 713

def self.disable_readonly_mode(key = READONLY_MODE_KEY)
  if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
    Sidekiq.unpause! if Sidekiq.paused?
  end

  Discourse.redis.del(key)
  MessageBus.publish(readonly_channel, false)
  true
end

.enable_pg_force_readonly_modeObject



723
724
725
726
727
728
729
# File 'lib/discourse.rb', line 723

def self.enable_pg_force_readonly_mode
  RailsMultisite::ConnectionManagement.each_connection do
    enable_readonly_mode(PG_FORCE_READONLY_MODE_KEY)
  end

  true
end

.enable_readonly_mode(key = READONLY_MODE_KEY) ⇒ Object



660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
# File 'lib/discourse.rb', line 660

def self.enable_readonly_mode(key = READONLY_MODE_KEY)
  if key == PG_READONLY_MODE_KEY || key == PG_FORCE_READONLY_MODE_KEY
    Sidekiq.pause!("pg_failover") if !Sidekiq.paused?
  end

  if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?(
       key,
     )
    Discourse.redis.set(key, 1)
  else
    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?
  end

  MessageBus.publish(readonly_channel, true)
  true
end

.enabled_auth_providersObject



493
494
495
# File 'lib/discourse.rb', line 493

def self.enabled_auth_providers
  auth_providers.select { |provider| provider.authenticator.enabled? }
end

.enabled_authenticatorsObject



503
504
505
# File 'lib/discourse.rb', line 503

def self.enabled_authenticators
  authenticators.select { |authenticator| authenticator.enabled? }
end

.filtersObject



315
316
317
# File 'lib/discourse.rb', line 315

def self.filters
  @filters ||= %i[latest unread new unseen top read posted bookmarks]
end

.find_compatible_git_resource(path) ⇒ Object

Find a compatible resource from a git repo



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/version.rb', line 93

def self.find_compatible_git_resource(path)
  return unless File.directory?("#{path}/.git")

  tree_info =
    Discourse::Utils.execute_command(
      "git",
      "-C",
      path,
      "ls-tree",
      "-l",
      "HEAD",
      Discourse::VERSION_COMPATIBILITY_FILENAME,
    )
  blob_size = tree_info.split[3].to_i

  if blob_size > Discourse::MAX_METADATA_FILE_SIZE
    $stderr.puts "#{Discourse::VERSION_COMPATIBILITY_FILENAME} file in #{path} too big"
    return
  end

  compat_resource =
    Discourse::Utils.execute_command(
      "git",
      "-C",
      path,
      "show",
      "HEAD@{upstream}:#{Discourse::VERSION_COMPATIBILITY_FILENAME}",
    )

  Discourse.find_compatible_resource(compat_resource)
rescue InvalidVersionListError => e
  $stderr.puts "Invalid version list in #{path}"
rescue Discourse::Utils::CommandError => e
  nil
end

.find_compatible_resource(version_list, target_version = ::Discourse::VERSION::STRING) ⇒ Object

lookup an external resource (theme/plugin)‘s best compatible version compatible resource files are YAML, in the format: `discourse_version: plugin/theme git reference.` For example:

2.5.0.beta6: c4a6c17
2.5.0.beta4: d1d2d3f
2.5.0.beta2: bbffee
2.4.4.beta6: some-other-branch-ref
2.4.2.beta1: v1-tag


38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/version.rb', line 38

def self.find_compatible_resource(version_list, target_version = ::Discourse::VERSION::STRING)
  return unless version_list.present?

  begin
    version_list = YAML.safe_load(version_list)
  rescue Psych::SyntaxError, Psych::DisallowedClass => e
  end

  raise InvalidVersionListError unless version_list.is_a?(Hash)

  version_list =
    version_list
      .transform_keys do |v|
        Gem::Requirement.parse(v)
      rescue Gem::Requirement::BadRequirementError => e
        raise InvalidVersionListError, "Invalid version specifier: #{v}"
      end
      .sort_by do |parsed_requirement, _|
        operator, version = parsed_requirement
        [version, operator == "<" ? 0 : 1]
      end

  parsed_target_version = Gem::Version.new(target_version)

  lowest_matching_entry =
    version_list.find do |parsed_requirement, target|
      req_operator, req_version = parsed_requirement
      req_operator = "<=" if req_operator == "="

      if !%w[<= <].include?(req_operator)
        raise InvalidVersionListError,
              "Invalid version specifier operator for '#{req_operator} #{req_version}'. Operator must be one of <= or <"
      end

      resolved_requirement = Gem::Requirement.new("#{req_operator} #{req_version.to_s}")
      resolved_requirement.satisfied_by?(parsed_target_version)
    end

  return if lowest_matching_entry.nil?

  checkout_version = lowest_matching_entry[1]

  begin
    Discourse::Utils.execute_command "git",
                                     "check-ref-format",
                                     "--allow-onelevel",
                                     checkout_version
  rescue RuntimeError
    raise InvalidVersionListError, "Invalid ref name: #{checkout_version}"
  end

  checkout_version
end

.find_plugin_css_assets(args) ⇒ Object



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/discourse.rb', line 417

def self.find_plugin_css_assets(args)
  plugins = apply_asset_filters(self.find_plugins(args), :css, args[:request])

  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

  assets.map! { |asset| "#{asset}_rtl" } if args[:rtl]
  assets
end

.find_plugin_js_assets(args) ⇒ Object



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
# File 'lib/discourse.rb', line 439

def self.find_plugin_js_assets(args)
  plugins =
    self
      .find_plugins(args)
      .select do |plugin|
        plugin.js_asset_exists? || plugin.extra_js_asset_exists? || plugin.admin_js_asset_exists?
      end

  plugins = apply_asset_filters(plugins, :js, args[:request])

  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?
    # TODO: make admin asset only load for admins
    assets << "plugins/#{plugin.directory_name}_admin" if plugin.admin_js_asset_exists?
    assets
  end
end

.find_plugins(args) ⇒ Object



392
393
394
395
396
397
398
399
400
# File 'lib/discourse.rb', line 392

def self.find_plugins(args)
  plugins.select do |plugin|
    next if args[:include_official] == false && plugin..official?
    next if args[:include_unofficial] == false && !plugin..official?
    next if !args[:include_disabled] && !plugin.enabled?

    true
  end
end

.full_versionObject



828
829
830
831
832
833
834
# File 'lib/discourse.rb', line 828

def self.full_version
  @full_version ||=
    begin
      git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null'
      self.try_git(git_cmd, "unknown")
    end
end

.git_branchObject



822
823
824
825
826
# File 'lib/discourse.rb', line 822

def self.git_branch
  @git_branch ||=
    self.try_git("git branch --show-current", nil) ||
      self.try_git("git config user.discourse-version", "unknown")
end

.git_versionObject



814
815
816
817
818
819
820
# File 'lib/discourse.rb', line 814

def self.git_version
  @git_version ||=
    begin
      git_cmd = "git rev-parse HEAD"
      self.try_git(git_cmd, Discourse::VERSION::STRING)
    end
end

.handle_job_exception(ex, context = {}, parent_logger = nil) ⇒ Object

Log an exception.

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. See app/jobs/base.rb for the error_context function.



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/discourse.rb', line 213

def self.handle_job_exception(ex, context = {}, parent_logger = nil)
  return if ex.class == Jobs::HandledExceptionWrapper

  context ||= {}
  parent_logger ||= Sidekiq

  job = context[:job]

  # mini_scheduler direct reporting
  if Hash === job
    job_class = job["class"]
    job_exception_stats[job_class] += 1 if job_class
  end

  # internal reporting
  job_exception_stats[job] += 1 if job.class == Class && ::Jobs::Base > job

  cm = RailsMultisite::ConnectionManagement
  parent_logger.handle_exception(
    ex,
    { current_db: cm.current_db, current_hostname: cm.current_hostname }.merge(context),
  )

  raise ex if Rails.env.test? && !@catch_job_exceptions
end

.has_needed_version?(current, needed) ⇒ Boolean

Returns:

  • (Boolean)


26
27
28
# File 'lib/version.rb', line 26

def self.has_needed_version?(current, needed)
  Gem::Version.new(current) >= Gem::Version.new(needed)
end

.is_cdn_request?(env, request_method) ⇒ Boolean

Returns:

  • (Boolean)


1167
1168
1169
1170
1171
1172
1173
1174
1175
# File 'lib/discourse.rb', line 1167

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

.is_parallel_test?Boolean

Returns:

  • (Boolean)


1161
1162
1163
# File 'lib/discourse.rb', line 1161

def self.is_parallel_test?
  ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"]
end

.job_exception_statsObject



185
186
187
# File 'lib/discourse.rb', line 185

def self.job_exception_stats
  @job_exception_stats
end

.keep_readonly_mode(key, ttl:) ⇒ Object



686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/discourse.rb', line 686

def self.keep_readonly_mode(key, ttl:)
  # extend the expiry by ttl minute every ttl/2 seconds
  @mutex ||= Mutex.new

  @mutex.synchronize do
    @dbs ||= Set.new
    @dbs << RailsMultisite::ConnectionManagement.current_db
    @threads ||= {}

    unless @threads[key]&.alive?
      @threads[key] = Thread.new do
        while @dbs.size > 0
          sleep ttl / 2

          @mutex.synchronize do
            @dbs.each do |db|
              RailsMultisite::ConnectionManagement.with_connection(db) do
                @dbs.delete(db) if !Discourse.redis.expire(key, ttl)
              end
            end
          end
        end
      end
    end
  end
end

.last_commit_dateObject



836
837
838
839
840
841
842
843
# File 'lib/discourse.rb', line 836

def self.last_commit_date
  @last_commit_date ||=
    begin
      git_cmd = 'git log -1 --format="%ct"'
      seconds = self.try_git(git_cmd, nil)
      seconds.nil? ? nil : DateTime.strptime(seconds, "%s")
    end
end

.official_pluginsObject



384
385
386
# File 'lib/discourse.rb', line 384

def self.official_plugins
  plugins.find_all { |p| p..official? }
end

.os_hostnameObject

hostname of the server, operating system level called os_hostname so we do no confuse it with current_hostname



520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
# File 'lib/discourse.rb', line 520

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

.pg_readonly_mode?Boolean

Returns:

  • (Boolean)


747
748
749
# File 'lib/discourse.rb', line 747

def self.pg_readonly_mode?
  Discourse.redis.get(PG_READONLY_MODE_KEY).present?
end

.plugin_themesObject



380
381
382
# File 'lib/discourse.rb', line 380

def self.plugin_themes
  @plugin_themes ||= plugins.map(&:themes).flatten
end

.pluginsObject



368
369
370
# File 'lib/discourse.rb', line 368

def self.plugins
  @plugins ||= []
end

.plugins_by_nameObject



372
373
374
# File 'lib/discourse.rb', line 372

def self.plugins_by_name
  @plugins_by_name ||= {}
end

.postgres_last_read_onlyObject

Shared between processes



752
753
754
# File 'lib/discourse.rb', line 752

def self.postgres_last_read_only
  @postgres_last_read_only ||= DistributedCache.new("postgres_last_read_only")
end

.postgres_recently_readonly?Boolean

Returns:

  • (Boolean)


761
762
763
764
765
766
# File 'lib/discourse.rb', line 761

def self.postgres_recently_readonly?
  seconds =
    postgres_last_read_only.defer_get_set("timestamp") { redis.get(LAST_POSTGRES_READONLY_KEY) }

  seconds ? Time.zone.at(seconds.to_i) > 15.seconds.ago : false
end

.preload_rails!Object

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



1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
# File 'lib/discourse.rb', line 1080

def self.preload_rails!
  return if @preloaded_rails

  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

    # load up all models and schema
    (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
      begin
        table.classify.constantize.first
      rescue StandardError
        nil
      end
    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
  end

  schema_cache = ActiveRecord::Base.connection.schema_cache

  RailsMultisite::ConnectionManagement.safe_each_connection do
    # load up schema cache for all multisite assuming all dbs have
    # an identical schema
    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")

    JsLocaleHelper.load_translations(SiteSetting.default_locale)
    Site.json_for(Guardian.new)
    SvgSprite.preload

    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
  end

  [
    Thread.new do
      # router warm up
      begin
        Rails.application.routes.recognize_path("abc")
      rescue StandardError
        nil
      end
    end,
    Thread.new do
      # preload discourse version
      Discourse.git_version
      Discourse.git_branch
      Discourse.full_version
      Discourse.plugins.each { |p| p.commit_url }
    end,
    Thread.new do
      require "actionview_precompiler"
      ActionviewPrecompiler.precompile
    end,
    Thread.new { LetterAvatar.image_magick_version },
    Thread.new { SvgSprite.core_svgs },
    Thread.new { EmberCli.script_chunks },
  ].each(&:join)
ensure
  @preloaded_rails = true
end

.privacy_policy_urlObject



621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
# File 'lib/discourse.rb', line 621

def self.privacy_policy_url
  if SiteSetting.privacy_policy_url.present?
    SiteSetting.privacy_policy_url
  else
    urls_cache["privacy_policy"] ||= (
      if SiteSetting.privacy_topic_id > 0 && Topic.exists?(id: SiteSetting.privacy_topic_id)
        "#{Discourse.base_path}/privacy"
      else
        :nil
      end
    )

    urls_cache["privacy_policy"] != :nil ? urls_cache["privacy_policy"] : nil
  end
end

.readonly_channelObject



896
897
898
# File 'lib/discourse.rb', line 896

def self.readonly_channel
  "/site/read-only"
end

.readonly_mode?(keys = READONLY_KEYS) ⇒ Boolean

Returns:

  • (Boolean)


739
740
741
# File 'lib/discourse.rb', line 739

def self.readonly_mode?(keys = READONLY_KEYS)
  recently_readonly? || GlobalSetting.pg_force_readonly_mode || Discourse.redis.exists?(*keys)
end

.received_postgres_readonly!Object



774
775
776
777
778
779
780
# File 'lib/discourse.rb', line 774

def self.received_postgres_readonly!
  time = Time.zone.now
  redis.set(LAST_POSTGRES_READONLY_KEY, time.to_i.to_s)
  postgres_last_read_only.clear(after_commit: false)

  time
end

.received_redis_readonly!Object



787
788
789
# File 'lib/discourse.rb', line 787

def self.received_redis_readonly!
  redis_last_read_only[Discourse.redis.namespace] = Time.zone.now
end

.recently_readonly?Boolean

Returns:

  • (Boolean)


768
769
770
771
772
# File 'lib/discourse.rb', line 768

def self.recently_readonly?
  redis_read_only = redis_last_read_only[Discourse.redis.namespace]

  (redis_read_only.present? && redis_read_only > 15.seconds.ago) || postgres_recently_readonly?
end

.redis_last_read_onlyObject

Per-process



757
758
759
# File 'lib/discourse.rb', line 757

def self.redis_last_read_only
  @redis_last_read_only ||= {}
end

.request_refresh!(user_ids: nil) ⇒ Object



802
803
804
805
806
807
808
809
810
811
812
# File 'lib/discourse.rb', line 802

def self.request_refresh!(user_ids: nil)
  # Causes refresh on next click for all clients
  #
  # This is better than `MessageBus.publish "/file-change", ["refresh"]` because
  # it spreads the refreshes out over a time period
  if user_ids
    MessageBus.publish("/refresh_client", "clobber", user_ids: user_ids)
  else
    MessageBus.publish("/global/asset-version", "clobber")
  end
end

.reset_active_record_cacheObject



1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
# File 'lib/discourse.rb', line 1058

def self.reset_active_record_cache
  ActiveRecord::Base.connection.query_cache.clear
  (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table|
    begin
      table.classify.constantize.reset_column_information
    rescue StandardError
      nil
    end
  end
  nil
end

.reset_active_record_cache_if_needed(e) ⇒ Object



1048
1049
1050
1051
1052
1053
1054
1055
1056
# File 'lib/discourse.rb', line 1048

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)
    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."
    Discourse.last_ar_cache_reset = Time.zone.now
    Discourse.reset_active_record_cache
  end
end

.reset_catch_job_exceptions!Object



201
202
203
204
# File 'lib/discourse.rb', line 201

def self.reset_catch_job_exceptions!
  raise "tests only" if !Rails.env.test?
  remove_instance_variable(:@catch_job_exceptions)
end

.reset_job_exception_stats!Object



189
190
191
# File 'lib/discourse.rb', line 189

def self.reset_job_exception_stats!
  @job_exception_stats = Hash.new(0)
end

.route_for(uri) ⇒ Object



574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/discourse.rb', line 574

def self.route_for(uri)
  unless uri.is_a?(URI)
    uri =
      begin
        URI(uri)
      rescue ArgumentError, URI::Error
      end
  end

  return unless uri

  path = +(uri.path || "")
  if !uri.host ||
       (uri.host == Discourse.current_hostname && path.start_with?(Discourse.base_path))
    path.slice!(Discourse.base_path)
    return Rails.application.routes.recognize_path(path)
  end

  nil
rescue ActionController::RoutingError
  nil
end

.running_in_rack?Boolean

Returns:

  • (Boolean)


1070
1071
1072
# File 'lib/discourse.rb', line 1070

def self.running_in_rack?
  ENV["DISCOURSE_RUNNING_IN_RACK"] == "1"
end

.sidekiq_redis_configObject



1036
1037
1038
1039
1040
# File 'lib/discourse.rb', line 1036

def self.sidekiq_redis_config
  conf = GlobalSetting.redis_config.dup
  conf[:namespace] = SIDEKIQ_NAMESPACE
  conf
end

.site_contact_userObject

Either returns the site_contact_username user or the first admin.



854
855
856
857
858
859
860
# File 'lib/discourse.rb', line 854

def self.site_contact_user
  user =
    User.find_by(
      username_lower: SiteSetting.site_contact_username.downcase,
    ) if SiteSetting.site_contact_username.present?
  user ||= (system_user || User.admins.real.order(:id).first)
end

.skip_post_deployment_migrations?Boolean

Returns:

  • (Boolean)


1074
1075
1076
# File 'lib/discourse.rb', line 1074

def self.skip_post_deployment_migrations?
  %w[1 true].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s)
end

.staff_writes_only_mode?Boolean

Returns:

  • (Boolean)


743
744
745
# File 'lib/discourse.rb', line 743

def self.staff_writes_only_mode?
  Discourse.redis.get(STAFF_WRITES_ONLY_MODE_KEY).present?
end

.static_doc_topic_idsObject



1042
1043
1044
# File 'lib/discourse.rb', line 1042

def self.static_doc_topic_ids
  [SiteSetting.tos_topic_id, SiteSetting.guidelines_topic_id, SiteSetting.privacy_topic_id]
end

.statsObject



880
881
882
# File 'lib/discourse.rb', line 880

def self.stats
  PluginStore.new("stats")
end

.storeObject



870
871
872
873
874
875
876
877
878
# File 'lib/discourse.rb', line 870

def self.store
  if SiteSetting.Upload.enable_s3_uploads
    @s3_store_loaded ||= require "file_store/s3_store"
    FileStore::S3Store.new
  else
    @local_store_loaded ||= require "file_store/local_store"
    FileStore::LocalStore.new
  end
end

.system_userObject



864
865
866
867
868
# File 'lib/discourse.rb', line 864

def self.system_user
  @system_users ||= {}
  current_db = RailsMultisite::ConnectionManagement.current_db
  @system_users[current_db] ||= User.find_by(id: SYSTEM_USER_ID)
end

.top_menu_itemsObject



323
324
325
# File 'lib/discourse.rb', line 323

def self.top_menu_items
  @top_menu_items ||= Discourse.filters + [:categories]
end

.tos_urlObject



605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
# File 'lib/discourse.rb', line 605

def self.tos_url
  if SiteSetting.tos_url.present?
    SiteSetting.tos_url
  else
    urls_cache["tos"] ||= (
      if SiteSetting.tos_topic_id > 0 && Topic.exists?(id: SiteSetting.tos_topic_id)
        "#{Discourse.base_path}/tos"
      else
        :nil
      end
    )

    urls_cache["tos"] != :nil ? urls_cache["tos"] : nil
  end
end

.try_git(git_cmd, default_value) ⇒ Object



845
846
847
848
849
850
851
# File 'lib/discourse.rb', line 845

def self.try_git(git_cmd, default_value)
  begin
    `#{git_cmd}`.strip
  rescue StandardError
    default_value
  end.presence || default_value
end

.unofficial_pluginsObject



388
389
390
# File 'lib/discourse.rb', line 388

def self.unofficial_plugins
  plugins.find_all { |p| !p..official? }
end

.urls_cacheObject



601
602
603
# File 'lib/discourse.rb', line 601

def self.urls_cache
  @urls_cache ||= DistributedCache.new("urls_cache")
end

.visible_pluginsObject



376
377
378
# File 'lib/discourse.rb', line 376

def self.visible_plugins
  plugins.filter(&:visible?)
end

.warn(message, env = nil) ⇒ Object

you can use Discourse.warn when you want to report custom environment with the error, this helps with grouping



937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
# File 'lib/discourse.rb', line 937

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

  logster_env = env

  if old_env = Thread.current[Logster::Logger::LOGSTER_ENV]
    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 }
  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

  if old_env
    env.each do |k, v|
      # do not leak state
      logster_env.delete(k)
    end
  end

  nil
end

.warn_exception(e, message: "", env: nil) ⇒ Object

report a warning maintaining backtrack for logster



977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'lib/discourse.rb', line 977

def self.warn_exception(e, message: "", env: nil)
  if Rails.logger.respond_to? :add_with_opts
    env ||= {}
    env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db

    # logster
    Rails.logger.add_with_opts(
      ::Logger::Severity::WARN,
      "#{message} : #{e.class.name} : #{e}",
      "discourse-exception",
      backtrace: e.backtrace.join("\n"),
      env: env,
    )
  else
    # no logster ... fallback
    Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}")
  end
rescue StandardError
  STDERR.puts "Failed to report exception #{e} #{message}"
end