diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js index cc85af1db8..c69c8ef051 100644 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ b/app/assets/javascripts/discourse/mixins/ajax.js @@ -1,13 +1,7 @@ /** This mixin provides an 'ajax' method that can be used to perform ajax requests that respect Discourse paths and the run loop. - - @class Discourse.Ajax - @extends Ember.Mixin - @namespace Discourse - @module Discourse **/ - var _trackView = false; Discourse.Ajax = Em.Mixin.create({ @@ -56,8 +50,14 @@ Discourse.Ajax = Em.Mixin.create({ args.headers['Discourse-Track-View'] = true; } - args.success = function(xhr) { - Ember.run(null, resolve, xhr); + args.success = function(data, textStatus, xhr) { + if (xhr.getResponseHeader('Discourse-Readonly')) { + Ember.run(function() { + Discourse.Site.currentProp('isReadOnly', true); + }); + } + + Ember.run(null, resolve, data); }; args.error = function(xhr, textStatus) { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 146fe5771b..f676d276e8 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -42,6 +42,7 @@ class ApplicationController < ActionController::Base before_filter :preload_json before_filter :check_xhr before_filter :redirect_to_login_if_required + after_filter :add_readonly_header layout :set_layout @@ -53,6 +54,10 @@ class ApplicationController < ActionController::Base @use_crawler_layout ||= (has_escaped_fragment? || CrawlerDetection.crawler?(request.user_agent)) end + def add_readonly_header + response.headers['Discourse-Readonly'] = 'true' if Discourse.readonly_mode? + end + def slow_platform? request.user_agent =~ /Android/ end @@ -394,7 +399,7 @@ class ApplicationController < ActionController::Base def block_if_readonly_mode return if request.fullpath.start_with?(path "/admin/backups") - raise Discourse::ReadOnly.new if !request.get? && Discourse.readonly_mode? + raise Discourse::ReadOnly.new if !(request.get? || request.head?) && Discourse.readonly_mode? end def build_not_found_page(status=404, layout=false) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 79eddd24ce..1bf579bc35 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -613,7 +613,7 @@ en: logout: "You were logged out." refresh: "Refresh" read_only_mode: - enabled: "An administrator enabled read-only mode. You can continue to browse the site but interactions may not work." + enabled: "Read-only mode is enabled. You can continue to browse the site but interactions may not work." login_disabled: "Login is disabled while the site is in read only mode." too_few_topics_notice: "Create at least 5 public topics and %{posts} public posts to get discussion started. New users cannot earn trust levels unless there's content for them to read. This message appears only to staff." diff --git a/lib/discourse.rb b/lib/discourse.rb index 304318ca4d..46f27f8fce 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -191,7 +191,7 @@ module Discourse end def self.readonly_mode? - !!$redis.get(readonly_mode_key) + DiscourseRedis.recently_readonly? || !!$redis.get(readonly_mode_key) end def self.request_refresh! diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index dd74515be4..b0a56d29ba 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -4,6 +4,19 @@ require_dependency 'cache' class DiscourseRedis + def self.recently_readonly? + return false unless @last_read_only + @last_read_only > 15.seconds.ago + end + + def self.received_readonly! + @last_read_only = Time.now + end + + def self.clear_readonly! + @last_read_only = nil + end + def self.raw_connection(config = nil) config ||= self.config redis_opts = {host: config['host'], port: config['port'], db: config['db']} @@ -38,7 +51,10 @@ class DiscourseRedis yield rescue Redis::CommandError => ex if ex.message =~ /READONLY/ - STDERR.puts "WARN: Redis is in a readonly state. Performed a noop" + unless DiscourseRedis.recently_readonly? + STDERR.puts "WARN: Redis is in a readonly state. Performed a noop" + end + DiscourseRedis.received_readonly! else raise ex end diff --git a/spec/components/discourse_spec.rb b/spec/components/discourse_spec.rb index 66ec12d977..09426698a9 100644 --- a/spec/components/discourse_spec.rb +++ b/spec/components/discourse_spec.rb @@ -107,16 +107,23 @@ describe Discourse do context "#readonly_mode?" do + after do + DiscourseRedis.clear_readonly! + end + + it "is false by default" do + expect(Discourse.readonly_mode?).to eq(false) + end + it "returns true when the key is present in redis" do $redis.expects(:get).with(Discourse.readonly_mode_key).returns("1") expect(Discourse.readonly_mode?).to eq(true) end - it "returns false when the key is not present in redis" do - $redis.expects(:get).with(Discourse.readonly_mode_key).returns(nil) - expect(Discourse.readonly_mode?).to eq(false) + it "returns true when DiscourseRedis is recently read only" do + DiscourseRedis.received_readonly! + expect(Discourse.readonly_mode?).to eq(true) end - end context "#handle_exception" do diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b42d1fa3a2..eb35d6caa0 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -107,6 +107,18 @@ describe TopicsController do end end + describe "read only header" do + it "returns no read only header by default" do + get :show, {topic_id: topic.id} + expect(response.headers['Discourse-Readonly']).to eq(nil) + end + + it "returns a readonly header if the site is read only" do + DiscourseRedis.received_readonly! + get :show, {topic_id: topic.id} + expect(response.headers['Discourse-Readonly']).to eq('true') + end + end end describe 'api' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2f888104a2..1f650b73be 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -113,6 +113,8 @@ Spork.prefork do # very expensive IO operations SiteSetting.automatically_download_gravatars = false + DiscourseRedis.clear_readonly! + I18n.locale = :en end