diff --git a/.streerc b/.streerc index b43ee5bb6e..fd138211ec 100644 --- a/.streerc +++ b/.streerc @@ -1,4 +1,3 @@ --print-width=100 --plugins=plugin/trailing_comma,disable_ternary --ignore-files=app/* ---ignore-files=lib/* diff --git a/lib/action_dispatch/session/discourse_cookie_store.rb b/lib/action_dispatch/session/discourse_cookie_store.rb index 91a32f4545..58da5c8f5d 100644 --- a/lib/action_dispatch/session/discourse_cookie_store.rb +++ b/lib/action_dispatch/session/discourse_cookie_store.rb @@ -9,9 +9,7 @@ class ActionDispatch::Session::DiscourseCookieStore < ActionDispatch::Session::C def set_cookie(request, session_id, cookie) if Hash === cookie - if SiteSetting.force_https - cookie[:secure] = true - end + cookie[:secure] = true if SiteSetting.force_https unless SiteSetting.same_site_cookies == "Disabled" cookie[:same_site] = SiteSetting.same_site_cookies end diff --git a/lib/admin_confirmation.rb b/lib/admin_confirmation.rb index fa73f66de7..06b7bc775c 100644 --- a/lib/admin_confirmation.rb +++ b/lib/admin_confirmation.rb @@ -17,10 +17,7 @@ class AdminConfirmation @token = SecureRandom.hex Discourse.redis.setex("admin-confirmation:#{@target_user.id}", 3.hours.to_i, @token) - payload = { - target_user_id: @target_user.id, - performed_by: @performed_by.id - } + payload = { target_user_id: @target_user.id, performed_by: @performed_by.id } Discourse.redis.setex("admin-confirmation-token:#{@token}", 3.hours.to_i, payload.to_json) Jobs.enqueue( @@ -28,7 +25,7 @@ class AdminConfirmation to_address: @performed_by.email, target_email: @target_user.email, target_username: @target_user.username, - token: @token + token: @token, ) end @@ -51,8 +48,8 @@ class AdminConfirmation return nil unless json parsed = JSON.parse(json) - target_user = User.find(parsed['target_user_id'].to_i) - performed_by = User.find(parsed['performed_by'].to_i) + target_user = User.find(parsed["target_user_id"].to_i) + performed_by = User.find(parsed["performed_by"].to_i) ac = AdminConfirmation.new(target_user, performed_by) ac.token = token diff --git a/lib/admin_constraint.rb b/lib/admin_constraint.rb index 7146eedfee..521cb0c2be 100644 --- a/lib/admin_constraint.rb +++ b/lib/admin_constraint.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminConstraint - def initialize(options = {}) @require_master = options[:require_master] end @@ -19,5 +18,4 @@ class AdminConstraint def custom_admin_check(request) true end - end diff --git a/lib/admin_user_index_query.rb b/lib/admin_user_index_query.rb index 9ecbb8f19a..49564110bf 100644 --- a/lib/admin_user_index_query.rb +++ b/lib/admin_user_index_query.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class AdminUserIndexQuery - def initialize(params = {}, klass = User, trust_levels = TrustLevel.levels) @params = params @query = initialize_query_with_order(klass) @@ -11,24 +10,22 @@ class AdminUserIndexQuery attr_reader :params, :trust_levels SORTABLE_MAPPING = { - 'created' => 'created_at', - 'last_emailed' => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'seen' => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", - 'username' => 'username', - 'email' => 'email', - 'trust_level' => 'trust_level', - 'days_visited' => 'user_stats.days_visited', - 'posts_read' => 'user_stats.posts_read_count', - 'topics_viewed' => 'user_stats.topics_entered', - 'posts' => 'user_stats.post_count', - 'read_time' => 'user_stats.time_read' + "created" => "created_at", + "last_emailed" => "COALESCE(last_emailed_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "seen" => "COALESCE(last_seen_at, to_date('1970-01-01', 'YYYY-MM-DD'))", + "username" => "username", + "email" => "email", + "trust_level" => "trust_level", + "days_visited" => "user_stats.days_visited", + "posts_read" => "user_stats.posts_read_count", + "topics_viewed" => "user_stats.topics_entered", + "posts" => "user_stats.post_count", + "read_time" => "user_stats.time_read", } def find_users(limit = 100) page = params[:page].to_i - 1 - if page < 0 - page = 0 - end + page = 0 if page < 0 find_users_query.limit(limit).offset(page * limit) end @@ -37,7 +34,13 @@ class AdminUserIndexQuery end def custom_direction - Discourse.deprecate(":ascending is deprecated please use :asc instead", output_in_test: true, drop_from: '2.9.0') if params[:ascending] + if params[:ascending] + Discourse.deprecate( + ":ascending is deprecated please use :asc instead", + output_in_test: true, + drop_from: "2.9.0", + ) + end asc = params[:asc] || params[:ascending] asc.present? && asc ? "ASC" : "DESC" end @@ -47,7 +50,7 @@ class AdminUserIndexQuery custom_order = params[:order] if custom_order.present? && - without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, '')] + without_dir = SORTABLE_MAPPING[custom_order.downcase.sub(/ (asc|desc)$/, "")] order << "#{without_dir} #{custom_direction}" end @@ -61,13 +64,9 @@ class AdminUserIndexQuery order << "users.username" end - query = klass - .includes(:totps) - .order(order.reject(&:blank?).join(",")) + query = klass.includes(:totps).order(order.reject(&:blank?).join(",")) - unless params[:stats].present? && params[:stats] == false - query = query.includes(:user_stat) - end + query = query.includes(:user_stat) unless params[:stats].present? && params[:stats] == false query = query.joins(:primary_email) if params[:show_emails] == "true" @@ -77,32 +76,44 @@ class AdminUserIndexQuery def filter_by_trust levels = trust_levels.map { |key, _| key.to_s } if levels.include?(params[:query]) - @query.where('trust_level = ?', trust_levels[params[:query].to_sym]) + @query.where("trust_level = ?", trust_levels[params[:query].to_sym]) end end def filter_by_query_classification case params[:query] - when 'staff' then @query.where("admin or moderator") - when 'admins' then @query.where(admin: true) - when 'moderators' then @query.where(moderator: true) - when 'silenced' then @query.silenced - when 'suspended' then @query.suspended - when 'pending' then @query.not_suspended.where(approved: false, active: true) - when 'staged' then @query.where(staged: true) + when "staff" + @query.where("admin or moderator") + when "admins" + @query.where(admin: true) + when "moderators" + @query.where(moderator: true) + when "silenced" + @query.silenced + when "suspended" + @query.suspended + when "pending" + @query.not_suspended.where(approved: false, active: true) + when "staged" + @query.where(staged: true) end end def filter_by_search if params[:email].present? - return @query.joins(:primary_email).where('user_emails.email = ?', params[:email].downcase) + return @query.joins(:primary_email).where("user_emails.email = ?", params[:email].downcase) end filter = params[:filter] if filter.present? filter = filter.strip - if ip = IPAddr.new(filter) rescue nil - @query.where('ip_address <<= :ip OR registration_ip_address <<= :ip', ip: ip.to_cidr_s) + if ip = + begin + IPAddr.new(filter) + rescue StandardError + nil + end + @query.where("ip_address <<= :ip OR registration_ip_address <<= :ip", ip: ip.to_cidr_s) else @query.filter_by_username_or_email(filter) end @@ -111,14 +122,12 @@ class AdminUserIndexQuery def filter_by_ip if params[:ip].present? - @query.where('ip_address = :ip OR registration_ip_address = :ip', ip: params[:ip].strip) + @query.where("ip_address = :ip OR registration_ip_address = :ip", ip: params[:ip].strip) end end def filter_exclude - if params[:exclude].present? - @query.where('users.id != ?', params[:exclude]) - end + @query.where("users.id != ?", params[:exclude]) if params[:exclude].present? end # this might not be needed in rails 4 ? @@ -134,5 +143,4 @@ class AdminUserIndexQuery append filter_by_search @query end - end diff --git a/lib/age_words.rb b/lib/age_words.rb index 814cc219e5..c9bfbfec28 100644 --- a/lib/age_words.rb +++ b/lib/age_words.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module AgeWords - def self.age_words(secs) if secs.blank? "—" @@ -10,5 +9,4 @@ module AgeWords FreedomPatches::Rails4.distance_of_time_in_words(now, now + secs) end end - end diff --git a/lib/archetype.rb b/lib/archetype.rb index e3fc8e5354..a9380d59d9 100644 --- a/lib/archetype.rb +++ b/lib/archetype.rb @@ -11,22 +11,19 @@ class Archetype end def attributes - { - id: @id, - options: @options - } + { id: @id, options: @options } end def self.default - 'regular' + "regular" end def self.private_message - 'private_message' + "private_message" end def self.banner - 'banner' + "banner" end def self.list @@ -40,8 +37,7 @@ class Archetype end # default archetypes - register 'regular' - register 'private_message' - register 'banner' - + register "regular" + register "private_message" + register "banner" end diff --git a/lib/auth.rb b/lib/auth.rb index f501d90157..5380c826d0 100644 --- a/lib/auth.rb +++ b/lib/auth.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -module Auth; end +module Auth +end -require 'auth/auth_provider' -require 'auth/result' -require 'auth/authenticator' -require 'auth/managed_authenticator' -require 'auth/facebook_authenticator' -require 'auth/github_authenticator' -require 'auth/twitter_authenticator' -require 'auth/google_oauth2_authenticator' -require 'auth/discord_authenticator' +require "auth/auth_provider" +require "auth/result" +require "auth/authenticator" +require "auth/managed_authenticator" +require "auth/facebook_authenticator" +require "auth/github_authenticator" +require "auth/twitter_authenticator" +require "auth/google_oauth2_authenticator" +require "auth/discord_authenticator" diff --git a/lib/auth/auth_provider.rb b/lib/auth/auth_provider.rb index 09f0f5f39a..20e0e6cfbc 100644 --- a/lib/auth/auth_provider.rb +++ b/lib/auth/auth_provider.rb @@ -8,32 +8,60 @@ class Auth::AuthProvider end def self.auth_attributes - [:authenticator, :pretty_name, :title, :message, :frame_width, :frame_height, - :pretty_name_setting, :title_setting, :enabled_setting, :full_screen_login, :full_screen_login_setting, - :custom_url, :background_color, :icon] + %i[ + authenticator + pretty_name + title + message + frame_width + frame_height + pretty_name_setting + title_setting + enabled_setting + full_screen_login + full_screen_login_setting + custom_url + background_color + icon + ] end attr_accessor(*auth_attributes) def enabled_setting=(val) - Discourse.deprecate("(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) enabled_setting is deprecated. Please define authenticator.enabled? instead", + drop_from: "2.9.0", + ) @enabled_setting = val end def background_color=(val) - Discourse.deprecate("(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) background_color is no longer functional. Please use CSS instead", + drop_from: "2.9.0", + ) end def full_screen_login=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def full_screen_login_setting=(val) - Discourse.deprecate("(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) full_screen_login is now forced. The full_screen_login_setting parameter can be removed from the auth_provider.", + drop_from: "2.9.0", + ) end def message=(val) - Discourse.deprecate("(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", drop_from: '2.9.0') + Discourse.deprecate( + "(#{authenticator.name}) message is no longer used because all logins are full screen. It should be removed from the auth_provider", + drop_from: "2.9.0", + ) end def name @@ -47,5 +75,4 @@ class Auth::AuthProvider def can_revoke authenticator.can_revoke? end - end diff --git a/lib/auth/current_user_provider.rb b/lib/auth/current_user_provider.rb index 31880b6dc6..f12e992de3 100644 --- a/lib/auth/current_user_provider.rb +++ b/lib/auth/current_user_provider.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -module Auth; end +module Auth +end class Auth::CurrentUserProvider - # do all current user initialization here def initialize(env) raise NotImplementedError diff --git a/lib/auth/default_current_user_provider.rb b/lib/auth/default_current_user_provider.rb index 3318b062da..91bacb78a7 100644 --- a/lib/auth/default_current_user_provider.rb +++ b/lib/auth/default_current_user_provider.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require_relative '../route_matcher' +require_relative "../route_matcher" # You may have seen references to v0 and v1 of our auth cookie in the codebase # and you're not sure how they differ, so here is an explanation: @@ -23,7 +23,6 @@ require_relative '../route_matcher' # We'll drop support for v0 after Discourse 2.9 is released. class Auth::DefaultCurrentUserProvider - CURRENT_USER_KEY ||= "_DISCOURSE_CURRENT_USER" USER_TOKEN_KEY ||= "_DISCOURSE_USER_TOKEN" API_KEY ||= "api_key" @@ -37,7 +36,7 @@ class Auth::DefaultCurrentUserProvider USER_API_CLIENT_ID ||= "HTTP_USER_API_CLIENT_ID" API_KEY_ENV ||= "_DISCOURSE_API" USER_API_KEY_ENV ||= "_DISCOURSE_USER_API" - TOKEN_COOKIE ||= ENV['DISCOURSE_TOKEN_COOKIE'] || "_t" + TOKEN_COOKIE ||= ENV["DISCOURSE_TOKEN_COOKIE"] || "_t" PATH_INFO ||= "PATH_INFO" COOKIE_ATTEMPTS_PER_MIN ||= 10 BAD_TOKEN ||= "_DISCOURSE_BAD_TOKEN" @@ -59,30 +58,20 @@ class Auth::DefaultCurrentUserProvider "badges#show", "tags#tag_feed", "tags#show", - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "list##{f}_feed" }, - *[:all, :yearly, :quarterly, :monthly, :weekly, :daily].map { |p| "list#top_#{p}_feed" }, - *[:latest, :unread, :new, :read, :posted, :bookmarks].map { |f| "tags#show_#{f}" } + *%i[latest unread new read posted bookmarks].map { |f| "list##{f}_feed" }, + *%i[all yearly quarterly monthly weekly daily].map { |p| "list#top_#{p}_feed" }, + *%i[latest unread new read posted bookmarks].map { |f| "tags#show_#{f}" }, ], - formats: :rss - ), - RouteMatcher.new( - methods: :get, - actions: "users#bookmarks", - formats: :ics - ), - RouteMatcher.new( - methods: :post, - actions: "admin/email#handle_mail", - formats: nil + formats: :rss, ), + RouteMatcher.new(methods: :get, actions: "users#bookmarks", formats: :ics), + RouteMatcher.new(methods: :post, actions: "admin/email#handle_mail", formats: nil), ] def self.find_v0_auth_cookie(request) cookie = request.cookies[TOKEN_COOKIE] - if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE - cookie - end + cookie if cookie&.valid_encoding? && cookie.present? && cookie.size == TOKEN_SIZE end def self.find_v1_auth_cookie(env) @@ -111,12 +100,10 @@ class Auth::DefaultCurrentUserProvider return @env[CURRENT_USER_KEY] if @env.key?(CURRENT_USER_KEY) # bypass if we have the shared session header - if shared_key = @env['HTTP_X_SHARED_SESSION_KEY'] + if shared_key = @env["HTTP_X_SHARED_SESSION_KEY"] uid = Discourse.redis.get("shared_session_key_#{shared_key}") user = nil - if uid - user = User.find_by(id: uid.to_i) - end + user = User.find_by(id: uid.to_i) if uid @env[CURRENT_USER_KEY] = user return user end @@ -130,28 +117,27 @@ class Auth::DefaultCurrentUserProvider user_api_key ||= request[PARAMETER_USER_API_KEY] end - if !@env.blank? && request[API_KEY] && api_parameter_allowed? - api_key ||= request[API_KEY] - end + api_key ||= request[API_KEY] if !@env.blank? && request[API_KEY] && api_parameter_allowed? auth_token = find_auth_token current_user = nil if auth_token - limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN , 60) + limiter = RateLimiter.new(nil, "cookie_auth_#{request.ip}", COOKIE_ATTEMPTS_PER_MIN, 60) if limiter.can_perform? - @env[USER_TOKEN_KEY] = @user_token = begin - UserAuthToken.lookup( - auth_token, - seen: true, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip - ) - rescue ActiveRecord::ReadOnlyError - nil - end + @env[USER_TOKEN_KEY] = @user_token = + begin + UserAuthToken.lookup( + auth_token, + seen: true, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + ) + rescue ActiveRecord::ReadOnlyError + nil + end current_user = @user_token.try(:user) end @@ -161,14 +147,10 @@ class Auth::DefaultCurrentUserProvider begin limiter.performed! rescue RateLimiter::LimitExceeded - raise Discourse::InvalidAccess.new( - 'Invalid Access', - nil, - delete_cookie: TOKEN_COOKIE - ) + raise Discourse::InvalidAccess.new("Invalid Access", nil, delete_cookie: TOKEN_COOKIE) end end - elsif @env['HTTP_DISCOURSE_LOGGED_IN'] + elsif @env["HTTP_DISCOURSE_LOGGED_IN"] @env[BAD_TOKEN] = true end @@ -177,10 +159,10 @@ class Auth::DefaultCurrentUserProvider current_user = lookup_api_user(api_key, request) if !current_user raise Discourse::InvalidAccess.new( - I18n.t('invalid_api_credentials'), - nil, - custom_message: "invalid_api_credentials" - ) + I18n.t("invalid_api_credentials"), + nil, + custom_message: "invalid_api_credentials", + ) end raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active admin_api_key_limiter.performed! if !Rails.env.profile? @@ -191,12 +173,13 @@ class Auth::DefaultCurrentUserProvider if user_api_key @hashed_user_api_key = ApiKey.hash_key(user_api_key) - user_api_key_obj = UserApiKey - .active - .joins(:user) - .where(key_hash: @hashed_user_api_key) - .includes(:user, :scopes) - .first + user_api_key_obj = + UserApiKey + .active + .joins(:user) + .where(key_hash: @hashed_user_api_key) + .includes(:user, :scopes) + .first raise Discourse::InvalidAccess unless user_api_key_obj @@ -208,18 +191,14 @@ class Auth::DefaultCurrentUserProvider current_user = user_api_key_obj.user raise Discourse::InvalidAccess if current_user.suspended? || !current_user.active - if can_write? - user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) - end + user_api_key_obj.update_last_used(@env[USER_API_CLIENT_ID]) if can_write? @env[USER_API_KEY_ENV] = true end # keep this rule here as a safeguard # under no conditions to suspended or inactive accounts get current_user - if current_user && (current_user.suspended? || !current_user.active) - current_user = nil - end + current_user = nil if current_user && (current_user.suspended? || !current_user.active) if current_user && should_update_last_seen? ip = request.ip @@ -247,31 +226,40 @@ class Auth::DefaultCurrentUserProvider if !is_user_api? && @user_token && @user_token.user == user rotated_at = @user_token.rotated_at - needs_rotation = @user_token.auth_token_seen ? rotated_at < UserAuthToken::ROTATE_TIME.ago : rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + needs_rotation = + ( + if @user_token.auth_token_seen + rotated_at < UserAuthToken::ROTATE_TIME.ago + else + rotated_at < UserAuthToken::URGENT_ROTATE_TIME.ago + end + ) if needs_rotation - if @user_token.rotate!(user_agent: @env['HTTP_USER_AGENT'], - client_ip: @request.ip, - path: @env['REQUEST_PATH']) + if @user_token.rotate!( + user_agent: @env["HTTP_USER_AGENT"], + client_ip: @request.ip, + path: @env["REQUEST_PATH"], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) DiscourseEvent.trigger(:user_session_refreshed, user) end end end - if !user && cookie_jar.key?(TOKEN_COOKIE) - cookie_jar.delete(TOKEN_COOKIE) - end + cookie_jar.delete(TOKEN_COOKIE) if !user && cookie_jar.key?(TOKEN_COOKIE) end def log_on_user(user, session, cookie_jar, opts = {}) - @env[USER_TOKEN_KEY] = @user_token = UserAuthToken.generate!( - user_id: user.id, - user_agent: @env['HTTP_USER_AGENT'], - path: @env['REQUEST_PATH'], - client_ip: @request.ip, - staff: user.staff?, - impersonate: opts[:impersonate]) + @env[USER_TOKEN_KEY] = @user_token = + UserAuthToken.generate!( + user_id: user.id, + user_agent: @env["HTTP_USER_AGENT"], + path: @env["REQUEST_PATH"], + client_ip: @request.ip, + staff: user.staff?, + impersonate: opts[:impersonate], + ) set_auth_cookie!(@user_token.unhashed_auth_token, user, cookie_jar) user.unstage! @@ -288,23 +276,19 @@ class Auth::DefaultCurrentUserProvider token: unhashed_auth_token, user_id: user.id, trust_level: user.trust_level, - issued_at: Time.zone.now.to_i + issued_at: Time.zone.now.to_i, } - if SiteSetting.persistent_sessions - expires = SiteSetting.maximum_session_age.hours.from_now - end + expires = SiteSetting.maximum_session_age.hours.from_now if SiteSetting.persistent_sessions - if SiteSetting.same_site_cookies != "Disabled" - same_site = SiteSetting.same_site_cookies - end + same_site = SiteSetting.same_site_cookies if SiteSetting.same_site_cookies != "Disabled" cookie_jar.encrypted[TOKEN_COOKIE] = { value: data, httponly: true, secure: SiteSetting.force_https, expires: expires, - same_site: same_site + same_site: same_site, } end @@ -313,10 +297,8 @@ class Auth::DefaultCurrentUserProvider # for signup flow, since all admin emails are stored in # DISCOURSE_DEVELOPER_EMAILS for self-hosters. def make_developer_admin(user) - if user.active? && - !user.admin && - Rails.configuration.respond_to?(:developer_emails) && - Rails.configuration.developer_emails.include?(user.email) + if user.active? && !user.admin && Rails.configuration.respond_to?(:developer_emails) && + Rails.configuration.developer_emails.include?(user.email) user.admin = true user.save Group.refresh_automatic_groups!(:staff, :admins) @@ -347,7 +329,7 @@ class Auth::DefaultCurrentUserProvider @user_token.destroy end - cookie_jar.delete('authentication_data') + cookie_jar.delete("authentication_data") cookie_jar.delete(TOKEN_COOKIE) end @@ -384,9 +366,7 @@ class Auth::DefaultCurrentUserProvider if api_key = ApiKey.active.with_key(api_key_value).includes(:user).first api_username = header_api_key? ? @env[HEADER_API_USERNAME] : request[API_USERNAME] - if !api_key.request_allowed?(@env) - return nil - end + return nil if !api_key.request_allowed?(@env) user = if api_key.user @@ -395,7 +375,8 @@ class Auth::DefaultCurrentUserProvider User.find_by(username_lower: api_username.downcase) elsif user_id = header_api_key? ? @env[HEADER_API_USER_ID] : request["api_user_id"] User.find_by(id: user_id.to_i) - elsif external_id = header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] + elsif external_id = + header_api_key? ? @env[HEADER_API_USER_EXTERNAL_ID] : request["api_user_external_id"] SingleSignOnRecord.find_by(external_id: external_id.to_s).try(:user) end @@ -435,52 +416,48 @@ class Auth::DefaultCurrentUserProvider limit = GlobalSetting.max_admin_api_reqs_per_minute.to_i if GlobalSetting.respond_to?(:max_admin_api_reqs_per_key_per_minute) - Discourse.deprecate("DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", drop_from: '2.9.0') - limit = [ - GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, - limit - ].max + Discourse.deprecate( + "DISCOURSE_MAX_ADMIN_API_REQS_PER_KEY_PER_MINUTE is deprecated. Please use DISCOURSE_MAX_ADMIN_API_REQS_PER_MINUTE", + drop_from: "2.9.0", + ) + limit = [GlobalSetting.max_admin_api_reqs_per_key_per_minute.to_i, limit].max end - @admin_api_key_limiter = RateLimiter.new( - nil, - "admin_api_min", - limit, - 60, - error_code: "admin_api_key_rate_limit" - ) + @admin_api_key_limiter = + RateLimiter.new(nil, "admin_api_min", limit, 60, error_code: "admin_api_key_rate_limit") end def user_api_key_limiter_60_secs - @user_api_key_limiter_60_secs ||= RateLimiter.new( - nil, - "user_api_min_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_minute, - 60, - error_code: "user_api_key_limiter_60_secs" - ) + @user_api_key_limiter_60_secs ||= + RateLimiter.new( + nil, + "user_api_min_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_minute, + 60, + error_code: "user_api_key_limiter_60_secs", + ) end def user_api_key_limiter_1_day - @user_api_key_limiter_1_day ||= RateLimiter.new( - nil, - "user_api_day_#{@hashed_user_api_key}", - GlobalSetting.max_user_api_reqs_per_day, - 86400, - error_code: "user_api_key_limiter_1_day" - ) + @user_api_key_limiter_1_day ||= + RateLimiter.new( + nil, + "user_api_day_#{@hashed_user_api_key}", + GlobalSetting.max_user_api_reqs_per_day, + 86_400, + error_code: "user_api_key_limiter_1_day", + ) end def find_auth_token return @auth_token if defined?(@auth_token) - @auth_token = begin - if v0 = self.class.find_v0_auth_cookie(@request) - v0 - elsif v1 = self.class.find_v1_auth_cookie(@env) - if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i - v1[:token] + @auth_token = + begin + if v0 = self.class.find_v0_auth_cookie(@request) + v0 + elsif v1 = self.class.find_v1_auth_cookie(@env) + v1[:token] if v1[:issued_at] >= SiteSetting.maximum_session_age.hours.ago.to_i end end - end end end diff --git a/lib/auth/discord_authenticator.rb b/lib/auth/discord_authenticator.rb index 2629e6d369..56d8923cd4 100644 --- a/lib/auth/discord_authenticator.rb +++ b/lib/auth/discord_authenticator.rb @@ -2,35 +2,34 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator class DiscordStrategy < OmniAuth::Strategies::OAuth2 - option :name, 'discord' - option :scope, 'identify email guilds' + option :name, "discord" + option :scope, "identify email guilds" option :client_options, - site: 'https://discord.com/api', - authorize_url: 'oauth2/authorize', - token_url: 'oauth2/token' + site: "https://discord.com/api", + authorize_url: "oauth2/authorize", + token_url: "oauth2/token" option :authorize_options, %i[scope permissions] - uid { raw_info['id'] } + uid { raw_info["id"] } info do { - name: raw_info['username'], - email: raw_info['verified'] ? raw_info['email'] : nil, - image: "https://cdn.discordapp.com/avatars/#{raw_info['id']}/#{raw_info['avatar']}" + name: raw_info["username"], + email: raw_info["verified"] ? raw_info["email"] : nil, + image: "https://cdn.discordapp.com/avatars/#{raw_info["id"]}/#{raw_info["avatar"]}", } end - extra do - { - 'raw_info' => raw_info - } - end + extra { { "raw_info" => raw_info } } def raw_info - @raw_info ||= access_token.get('users/@me').parsed. - merge(guilds: access_token.get('users/@me/guilds').parsed) + @raw_info ||= + access_token + .get("users/@me") + .parsed + .merge(guilds: access_token.get("users/@me/guilds").parsed) end def callback_url @@ -39,7 +38,7 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator end def name - 'discord' + "discord" end def enabled? @@ -48,23 +47,26 @@ class Auth::DiscordAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider DiscordStrategy, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.discord_client_id - strategy.options[:client_secret] = SiteSetting.discord_secret - } - end + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.discord_client_id + strategy.options[:client_secret] = SiteSetting.discord_secret + } + end def after_authenticate(auth_token, existing_account: nil) allowed_guild_ids = SiteSetting.discord_trusted_guilds.split("|") if allowed_guild_ids.length > 0 - user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g['id'] } + user_guild_ids = auth_token.extra[:raw_info][:guilds].map { |g| g["id"] } if (user_guild_ids & allowed_guild_ids).empty? # User is not in any allowed guilds - return Auth::Result.new.tap do |auth_result| - auth_result.failed = true - auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") - end + return( + Auth::Result.new.tap do |auth_result| + auth_result.failed = true + auth_result.failed_reason = I18n.t("discord.not_in_allowed_guild") + end + ) end end diff --git a/lib/auth/facebook_authenticator.rb b/lib/auth/facebook_authenticator.rb index 48a57cd9ae..18924c5fc0 100644 --- a/lib/auth/facebook_authenticator.rb +++ b/lib/auth/facebook_authenticator.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator - AVATAR_SIZE ||= 480 def name @@ -14,15 +13,19 @@ class Auth::FacebookAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :facebook, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.facebook_app_id - strategy.options[:client_secret] = SiteSetting.facebook_app_secret - strategy.options[:info_fields] = 'name,first_name,last_name,email' - strategy.options[:image_size] = { width: AVATAR_SIZE, height: AVATAR_SIZE } - strategy.options[:secure_image_url] = true - }, - scope: "email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.facebook_app_id + strategy.options[:client_secret] = SiteSetting.facebook_app_secret + strategy.options[:info_fields] = "name,first_name,last_name,email" + strategy.options[:image_size] = { + width: AVATAR_SIZE, + height: AVATAR_SIZE, + } + strategy.options[:secure_image_url] = true + }, + scope: "email" end # facebook doesn't return unverified email addresses so it's safe to assume diff --git a/lib/auth/github_authenticator.rb b/lib/auth/github_authenticator.rb index 865cb21e0c..03f2913f27 100644 --- a/lib/auth/github_authenticator.rb +++ b/lib/auth/github_authenticator.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -require 'has_errors' +require "has_errors" class Auth::GithubAuthenticator < Auth::ManagedAuthenticator - def name "github" end @@ -50,12 +49,13 @@ class Auth::GithubAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :github, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.github_client_id - strategy.options[:client_secret] = SiteSetting.github_client_secret - }, - scope: "user:email" + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.github_client_id + strategy.options[:client_secret] = SiteSetting.github_client_secret + }, + scope: "user:email" end # the omniauth-github gem only picks up the primary email if it's verified: diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index ba35b1fecd..3e7c4c4afb 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -22,47 +22,46 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) options = { - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:client_id] = SiteSetting.google_oauth2_client_id - strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret + setup: + lambda do |env| + strategy = env["omniauth.strategy"] + strategy.options[:client_id] = SiteSetting.google_oauth2_client_id + strategy.options[:client_secret] = SiteSetting.google_oauth2_client_secret - if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? - strategy.options[:hd] = google_oauth2_hd - end + if (google_oauth2_hd = SiteSetting.google_oauth2_hd).present? + strategy.options[:hd] = google_oauth2_hd + end - if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? - strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") - end + if (google_oauth2_prompt = SiteSetting.google_oauth2_prompt).present? + strategy.options[:prompt] = google_oauth2_prompt.gsub("|", " ") + end - # All the data we need for the `info` and `credentials` auth hash - # are obtained via the user info API, not the JWT. Using and verifying - # the JWT can fail due to clock skew, so let's skip it completely. - # https://github.com/zquestz/omniauth-google-oauth2/pull/392 - strategy.options[:skip_jwt] = true - } + # All the data we need for the `info` and `credentials` auth hash + # are obtained via the user info API, not the JWT. Using and verifying + # the JWT can fail due to clock skew, so let's skip it completely. + # https://github.com/zquestz/omniauth-google-oauth2/pull/392 + strategy.options[:skip_jwt] = true + end, } omniauth.provider :google_oauth2, options end def after_authenticate(auth_token, existing_account: nil) groups = provides_groups? ? raw_groups(auth_token.uid) : nil - if groups - auth_token.extra[:raw_groups] = groups - end + auth_token.extra[:raw_groups] = groups if groups result = super if groups - result.associated_groups = groups.map { |group| group.with_indifferent_access.slice(:id, :name) } + result.associated_groups = + groups.map { |group| group.with_indifferent_access.slice(:id, :name) } end result end def provides_groups? - SiteSetting.google_oauth2_hd.present? && - SiteSetting.google_oauth2_hd_groups && + SiteSetting.google_oauth2_hd.present? && SiteSetting.google_oauth2_hd_groups && SiteSetting.google_oauth2_hd_groups_service_account_admin_email.present? && SiteSetting.google_oauth2_hd_groups_service_account_json.present? end @@ -77,20 +76,20 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator return if client.nil? loop do - params = { - userKey: uid - } + params = { userKey: uid } params[:pageToken] = page_token if page_token response = client.get(groups_url, params: params, raise_errors: false) if response.status == 200 response = response.parsed - groups.push(*response['groups']) - page_token = response['nextPageToken'] + groups.push(*response["groups"]) + page_token = response["nextPageToken"] break if page_token.nil? else - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve groups for #{uid} - status #{response.status}", + ) break end end @@ -107,26 +106,35 @@ class Auth::GoogleOAuth2Authenticator < Auth::ManagedAuthenticator scope: GROUPS_SCOPE, iat: Time.now.to_i, exp: Time.now.to_i + 60, - sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email + sub: SiteSetting.google_oauth2_hd_groups_service_account_admin_email, } headers = { "alg" => "RS256", "typ" => "JWT" } key = OpenSSL::PKey::RSA.new(service_account_info["private_key"]) - encoded_jwt = ::JWT.encode(payload, key, 'RS256', headers) + encoded_jwt = ::JWT.encode(payload, key, "RS256", headers) - client = OAuth2::Client.new( - SiteSetting.google_oauth2_client_id, - SiteSetting.google_oauth2_client_secret, - site: OAUTH2_BASE_URL - ) + client = + OAuth2::Client.new( + SiteSetting.google_oauth2_client_id, + SiteSetting.google_oauth2_client_secret, + site: OAUTH2_BASE_URL, + ) - token_response = client.request(:post, '/token', body: { - grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", - assertion: encoded_jwt - }, raise_errors: false) + token_response = + client.request( + :post, + "/token", + body: { + grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", + assertion: encoded_jwt, + }, + raise_errors: false, + ) if token_response.status != 200 - Rails.logger.error("[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}") + Rails.logger.error( + "[Discourse Google OAuth2] failed to retrieve group fetch token - status #{token_response.status}", + ) return end diff --git a/lib/auth/managed_authenticator.rb b/lib/auth/managed_authenticator.rb index 41f36a611f..bb163d1a71 100644 --- a/lib/auth/managed_authenticator.rb +++ b/lib/auth/managed_authenticator.rb @@ -56,28 +56,27 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_authenticate(auth_token, existing_account: nil) # Try and find an association for this account - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) # Reconnecting to existing account - if can_connect_existing_user? && existing_account && (association.user.nil? || existing_account.id != association.user_id) + if can_connect_existing_user? && existing_account && + (association.user.nil? || existing_account.id != association.user_id) association.user = existing_account end # Matching an account by email - if match_by_email && - association.user.nil? && - (user = find_user_by_email(auth_token)) - + if match_by_email && association.user.nil? && (user = find_user_by_email(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end # Matching an account by username - if match_by_username && - association.user.nil? && - SiteSetting.username_change_period.zero? && - (user = find_user_by_username(auth_token)) - + if match_by_username && association.user.nil? && SiteSetting.username_change_period.zero? && + (user = find_user_by_username(auth_token)) UserAssociatedAccount.where(user: user, provider_name: auth_token[:provider]).destroy_all # Destroy existing associations for the new user association.user = user end @@ -100,7 +99,14 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result = Auth::Result.new info = auth_token[:info] result.email = info[:email] - result.name = (info[:first_name] && info[:last_name]) ? "#{info[:first_name]} #{info[:last_name]}" : info[:name] + result.name = + ( + if (info[:first_name] && info[:last_name]) + "#{info[:first_name]} #{info[:last_name]}" + else + info[:name] + end + ) if result.name.present? && result.name == result.email # Some IDPs send the email address in the name parameter (e.g. Auth0 with default configuration) # We add some generic protection here, so that users don't accidently make their email addresses public @@ -109,10 +115,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator result.username = info[:nickname] result.email_valid = primary_email_verified?(auth_token) if result.email.present? result.overrides_email = always_update_user_email? - result.extra_data = { - provider: auth_token[:provider], - uid: auth_token[:uid] - } + result.extra_data = { provider: auth_token[:provider], uid: auth_token[:uid] } result.user = association.user result @@ -120,7 +123,11 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def after_create_account(user, auth_result) auth_token = auth_result[:extra_data] - association = UserAssociatedAccount.find_or_initialize_by(provider_name: auth_token[:provider], provider_uid: auth_token[:uid]) + association = + UserAssociatedAccount.find_or_initialize_by( + provider_name: auth_token[:provider], + provider_uid: auth_token[:uid], + ) association.user = user association.save! @@ -132,16 +139,12 @@ class Auth::ManagedAuthenticator < Auth::Authenticator def find_user_by_email(auth_token) email = auth_token.dig(:info, :email) - if email && primary_email_verified?(auth_token) - User.find_by_email(email) - end + User.find_by_email(email) if email && primary_email_verified?(auth_token) end def find_user_by_username(auth_token) username = auth_token.dig(:info, :nickname) - if username - User.find_by_username(username) - end + User.find_by_username(username) if username end def retrieve_avatar(user, url) @@ -158,7 +161,7 @@ class Auth::ManagedAuthenticator < Auth::Authenticator if bio || location profile = user.user_profile - profile.bio_raw = bio unless profile.bio_raw.present? + profile.bio_raw = bio unless profile.bio_raw.present? profile.location = location unless profile.location.present? profile.save end diff --git a/lib/auth/result.rb b/lib/auth/result.rb index 397a373f80..8e83fea8ba 100644 --- a/lib/auth/result.rb +++ b/lib/auth/result.rb @@ -1,48 +1,48 @@ # frozen_string_literal: true class Auth::Result - ATTRIBUTES = [ - :user, - :name, - :username, - :email, - :email_valid, - :extra_data, - :awaiting_activation, - :awaiting_approval, - :authenticated, - :authenticator_name, - :requires_invite, - :not_allowed_from_ip_address, - :admin_not_allowed_from_ip_address, - :skip_email_validation, - :destination_url, - :omniauth_disallow_totp, - :failed, - :failed_reason, - :failed_code, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + ATTRIBUTES = %i[ + user + name + username + email + email_valid + extra_data + awaiting_activation + awaiting_approval + authenticated + authenticator_name + requires_invite + not_allowed_from_ip_address + admin_not_allowed_from_ip_address + skip_email_validation + destination_url + omniauth_disallow_totp + failed + failed_reason + failed_code + associated_groups + overrides_email + overrides_username + overrides_name ] attr_accessor *ATTRIBUTES # These are stored in the session during # account creation. The user cannot read or modify them - SESSION_ATTRIBUTES = [ - :email, - :username, - :email_valid, - :name, - :authenticator_name, - :extra_data, - :skip_email_validation, - :associated_groups, - :overrides_email, - :overrides_username, - :overrides_name, + SESSION_ATTRIBUTES = %i[ + email + username + email_valid + name + authenticator_name + extra_data + skip_email_validation + associated_groups + overrides_email + overrides_username + overrides_name ] def [](key) @@ -59,9 +59,7 @@ class Auth::Result end def email_valid=(val) - if !val.in? [true, false, nil] - raise ArgumentError, "email_valid should be boolean or nil" - end + raise ArgumentError, "email_valid should be boolean or nil" if !val.in? [true, false, nil] @email_valid = !!val end @@ -83,14 +81,14 @@ class Auth::Result def apply_user_attributes! change_made = false - if (SiteSetting.auth_overrides_username? || overrides_username) && (resolved_username = resolve_username).present? + if (SiteSetting.auth_overrides_username? || overrides_username) && + (resolved_username = resolve_username).present? change_made = UsernameChanger.override(user, resolved_username) end - if (SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid")) && - email_valid && - email.present? && - user.email != Email.downcase(email) + if ( + SiteSetting.auth_overrides_email || overrides_email || user&.email&.ends_with?(".invalid") + ) && email_valid && email.present? && user.email != Email.downcase(email) user.email = email change_made = true end @@ -109,11 +107,12 @@ class Auth::Result associated_groups.uniq.each do |associated_group| begin - associated_group = AssociatedGroup.find_or_create_by( - name: associated_group[:name], - provider_id: associated_group[:id], - provider_name: extra_data[:provider] - ) + associated_group = + AssociatedGroup.find_or_create_by( + name: associated_group[:name], + provider_id: associated_group[:id], + provider_name: extra_data[:provider], + ) rescue ActiveRecord::RecordNotUnique retry end @@ -135,22 +134,12 @@ class Auth::Result end def to_client_hash - if requires_invite - return { requires_invite: true } - end + return { requires_invite: true } if requires_invite - if user&.suspended? - return { - suspended: true, - suspended_message: user.suspended_message - } - end + return { suspended: true, suspended_message: user.suspended_message } if user&.suspended? if omniauth_disallow_totp - return { - omniauth_disallow_totp: !!omniauth_disallow_totp, - email: email - } + return { omniauth_disallow_totp: !!omniauth_disallow_totp, email: email } end if user @@ -159,7 +148,7 @@ class Auth::Result awaiting_activation: !!awaiting_activation, awaiting_approval: !!awaiting_approval, not_allowed_from_ip_address: !!not_allowed_from_ip_address, - admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address + admin_not_allowed_from_ip_address: !!admin_not_allowed_from_ip_address, } result[:destination_url] = destination_url if authenticated && destination_url.present? @@ -173,7 +162,7 @@ class Auth::Result auth_provider: authenticator_name, email_valid: !!email_valid, can_edit_username: can_edit_username, - can_edit_name: can_edit_name + can_edit_name: can_edit_name, } result[:destination_url] = destination_url if destination_url.present? @@ -190,9 +179,7 @@ class Auth::Result def staged_user return @staged_user if defined?(@staged_user) - if email.present? && email_valid - @staged_user = User.where(staged: true).find_by_email(email) - end + @staged_user = User.where(staged: true).find_by_email(email) if email.present? && email_valid end def username_suggester_attributes diff --git a/lib/auth/twitter_authenticator.rb b/lib/auth/twitter_authenticator.rb index 35f990bb66..5817f617e3 100644 --- a/lib/auth/twitter_authenticator.rb +++ b/lib/auth/twitter_authenticator.rb @@ -17,11 +17,12 @@ class Auth::TwitterAuthenticator < Auth::ManagedAuthenticator def register_middleware(omniauth) omniauth.provider :twitter, - setup: lambda { |env| - strategy = env["omniauth.strategy"] - strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key - strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret - } + setup: + lambda { |env| + strategy = env["omniauth.strategy"] + strategy.options[:consumer_key] = SiteSetting.twitter_consumer_key + strategy.options[:consumer_secret] = SiteSetting.twitter_consumer_secret + } end # twitter doesn't return unverfied email addresses in the API diff --git a/lib/autospec/base_runner.rb b/lib/autospec/base_runner.rb index bc78371f5c..4a0128f4cc 100644 --- a/lib/autospec/base_runner.rb +++ b/lib/autospec/base_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class BaseRunner - # used when starting the runner - preloading happens here def start(opts = {}) end @@ -32,7 +30,5 @@ module Autospec # used to stop the runner def stop end - end - end diff --git a/lib/autospec/formatter.rb b/lib/autospec/formatter.rb index a6b26aff7f..5dee7dd42c 100644 --- a/lib/autospec/formatter.rb +++ b/lib/autospec/formatter.rb @@ -3,11 +3,15 @@ require "rspec/core/formatters/base_text_formatter" require "parallel_tests/rspec/logger_base" -module Autospec; end +module Autospec +end class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter - - RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump + RSpec::Core::Formatters.register self, + :example_passed, + :example_pending, + :example_failed, + :start_dump RSPEC_RESULT = "./tmp/rspec_result" @@ -19,15 +23,15 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter end def example_passed(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('.', :success) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap(".", :success) end def example_pending(_notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('*', :pending) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("*", :pending) end def example_failed(notification) - output.print RSpec::Core::Formatters::ConsoleCodes.wrap('F', :failure) + output.print RSpec::Core::Formatters::ConsoleCodes.wrap("F", :failure) @fail_file.puts(notification.example.location + " ") @fail_file.flush end @@ -40,5 +44,4 @@ class Autospec::Formatter < RSpec::Core::Formatters::BaseTextFormatter @fail_file.close super(filename) end - end diff --git a/lib/autospec/manager.rb b/lib/autospec/manager.rb index e912887d2e..87dded1963 100644 --- a/lib/autospec/manager.rb +++ b/lib/autospec/manager.rb @@ -7,7 +7,8 @@ require "autospec/reload_css" require "autospec/base_runner" require "socket_server" -module Autospec; end +module Autospec +end class Autospec::Manager def self.run(opts = {}) @@ -25,7 +26,10 @@ class Autospec::Manager end def run - Signal.trap("HUP") { stop_runners; exit } + Signal.trap("HUP") do + stop_runners + exit + end Signal.trap("INT") do begin @@ -47,7 +51,6 @@ class Autospec::Manager STDIN.gets process_queue end - rescue => e fail(e, "failed in run") ensure @@ -71,16 +74,16 @@ class Autospec::Manager @queue.reject! { |_, s, _| s == "spec" } - if current_runner - @queue.concat [['spec', 'spec', current_runner]] - end + @queue.concat [["spec", "spec", current_runner]] if current_runner @runners.each do |runner| - @queue.concat [['spec', 'spec', runner]] unless @queue.any? { |_, s, r| s == "spec" && r == runner } + unless @queue.any? { |_, s, r| s == "spec" && r == runner } + @queue.concat [["spec", "spec", runner]] + end end end - [:start, :stop, :abort].each do |verb| + %i[start stop abort].each do |verb| define_method("#{verb}_runners") do puts "@@@@@@@@@@@@ #{verb}_runners" if @debug @runners.each(&verb) @@ -89,11 +92,7 @@ class Autospec::Manager def start_service_queue puts "@@@@@@@@@@@@ start_service_queue" if @debug - Thread.new do - while true - thread_loop - end - end + Thread.new { thread_loop while true } end # the main loop, will run the specs in the queue till one fails or the queue is empty @@ -176,9 +175,7 @@ class Autospec::Manager Dir[root_path + "/plugins/*"].each do |f| next if !File.directory? f resolved = File.realpath(f) - if resolved != f - map[resolved] = f - end + map[resolved] = f if resolved != f end map end @@ -188,9 +185,7 @@ class Autospec::Manager resolved = file @reverse_map ||= reverse_symlink_map @reverse_map.each do |location, discourse_location| - if file.start_with?(location) - resolved = discourse_location + file[location.length..-1] - end + resolved = discourse_location + file[location.length..-1] if file.start_with?(location) end resolved @@ -199,9 +194,7 @@ class Autospec::Manager def listen_for_changes puts "@@@@@@@@@@@@ listen_for_changes" if @debug - options = { - ignore: /^lib\/autospec/, - } + options = { ignore: %r{^lib/autospec} } if @opts[:force_polling] options[:force_polling] = true @@ -210,14 +203,14 @@ class Autospec::Manager path = root_path - if ENV['VIM_AUTOSPEC'] + if ENV["VIM_AUTOSPEC"] STDERR.puts "Using VIM file listener" socket_path = (Rails.root + "tmp/file_change.sock").to_s FileUtils.rm_f(socket_path) server = SocketServer.new(socket_path) server.start do |line| - file, line = line.split(' ') + file, line = line.split(" ") file = reverse_symlink(file) file = file.sub(Rails.root.to_s + "/", "") # process_change can acquire a mutex and block @@ -235,20 +228,20 @@ class Autospec::Manager end # to speed up boot we use a thread - ["spec", "lib", "app", "config", "test", "vendor", "plugins"].each do |watch| - + %w[spec lib app config test vendor plugins].each do |watch| puts "@@@@@@@@@ Listen to #{path}/#{watch} #{options}" if @debug Thread.new do begin - listener = Listen.to("#{path}/#{watch}", options) do |modified, added, _| - paths = [modified, added].flatten - paths.compact! - paths.map! do |long| - long = reverse_symlink(long) - long[(path.length + 1)..-1] + listener = + Listen.to("#{path}/#{watch}", options) do |modified, added, _| + paths = [modified, added].flatten + paths.compact! + paths.map! do |long| + long = reverse_symlink(long) + long[(path.length + 1)..-1] + end + process_change(paths) end - process_change(paths) - end listener.start sleep rescue => e @@ -257,7 +250,6 @@ class Autospec::Manager end end end - end def process_change(files) @@ -285,13 +277,9 @@ class Autospec::Manager hit = true spec = v ? (v.arity == 1 ? v.call(m) : v.call) : file with_line = spec - if spec == file && line - with_line = spec + ":" << line.to_s - end + with_line = spec + ":" << line.to_s if spec == file && line if File.exist?(spec) || Dir.exist?(spec) - if with_line != spec - specs << [file, spec, runner] - end + specs << [file, spec, runner] if with_line != spec specs << [file, with_line, runner] end end @@ -329,9 +317,7 @@ class Autospec::Manager focus = @queue.shift @queue.unshift([file, spec, runner]) unless spec.include?(":") && focus[1].include?(spec.split(":")[0]) - if focus[1].include?(spec) || file != spec - @queue.unshift(focus) - end + @queue.unshift(focus) if focus[1].include?(spec) || file != spec end else @queue.unshift([file, spec, runner]) diff --git a/lib/autospec/reload_css.rb b/lib/autospec/reload_css.rb index 8f24cdec98..78258bed17 100644 --- a/lib/autospec/reload_css.rb +++ b/lib/autospec/reload_css.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -module Autospec; end +module Autospec +end class Autospec::ReloadCss - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -30,7 +30,7 @@ class Autospec::ReloadCss if paths.any? { |p| p =~ /\.(css|s[ac]ss)/ } # todo connect to dev instead? ActiveRecord::Base.establish_connection - [:desktop, :mobile].each do |style| + %i[desktop mobile].each do |style| s = DiscourseStylesheets.new(style) s.compile paths << "public" + s.stylesheet_relpath_no_digest @@ -44,10 +44,9 @@ class Autospec::ReloadCss p = p.sub(/\.sass\.erb/, "") p = p.sub(/\.sass/, "") p = p.sub(/\.scss/, "") - p = p.sub(/^app\/assets\/stylesheets/, "assets") + p = p.sub(%r{^app/assets/stylesheets}, "assets") { name: p, hash: hash || SecureRandom.hex } end message_bus.publish "/file-change", paths end - end diff --git a/lib/autospec/rspec_runner.rb b/lib/autospec/rspec_runner.rb index b4ebbf3ca2..408b6fe79e 100644 --- a/lib/autospec/rspec_runner.rb +++ b/lib/autospec/rspec_runner.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true module Autospec - class RspecRunner < BaseRunner - WATCHERS = {} def self.watch(pattern, &blk) WATCHERS[pattern] = blk @@ -13,26 +11,28 @@ module Autospec end # Discourse specific - watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/components/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } - watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.+)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } watch(%r{^spec/.+_spec\.rb$}) - watch(%r{^spec/support/.+\.rb$}) { "spec" } - watch("app/controllers/application_controller.rb") { "spec/requests" } + watch(%r{^spec/support/.+\.rb$}) { "spec" } + watch("app/controllers/application_controller.rb") { "spec/requests" } - watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{app/controllers/(.+).rb}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{^app/views/(.+)/.+\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } - watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } + watch(%r{^spec/fabricators/.+_fabricator\.rb$}) { "spec" } - watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } + watch(%r{^app/assets/javascripts/pretty-text/.*\.js\.es6$}) do + "spec/components/pretty_text_spec.rb" + end watch(%r{^plugins/.*/discourse-markdown/.*\.js\.es6$}) { "spec/components/pretty_text_spec.rb" } watch(%r{^plugins/.*/spec/.*\.rb}) - watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } - watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } + watch(%r{^(plugins/.*/)plugin\.rb}) { |m| "#{m[1]}spec" } + watch(%r{^(plugins/.*)/(lib|app)}) { |m| "#{m[1]}/spec/integration" } watch(%r{^(plugins/.*)/lib/(.*)\.rb}) { |m| "#{m[1]}/spec/lib/#{m[2]}_spec.rb" } RELOADERS = Set.new @@ -50,11 +50,9 @@ module Autospec def failed_specs specs = [] - path = './tmp/rspec_result' + path = "./tmp/rspec_result" specs = File.readlines(path) if File.exist?(path) specs end - end - end diff --git a/lib/autospec/simple_runner.rb b/lib/autospec/simple_runner.rb index 62fcbc21a0..dcf88e4443 100644 --- a/lib/autospec/simple_runner.rb +++ b/lib/autospec/simple_runner.rb @@ -3,7 +3,6 @@ require "autospec/rspec_runner" module Autospec - class SimpleRunner < RspecRunner def initialize @mutex = Mutex.new @@ -12,36 +11,29 @@ module Autospec def run(specs) puts "Running Rspec: #{specs}" # kill previous rspec instance - @mutex.synchronize do - self.abort - end + @mutex.synchronize { self.abort } # we use our custom rspec formatter - args = [ - "-r", "#{File.dirname(__FILE__)}/formatter.rb", - "-f", "Autospec::Formatter" - ] + args = ["-r", "#{File.dirname(__FILE__)}/formatter.rb", "-f", "Autospec::Formatter"] - command = begin - line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line - multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files - if ENV["PARALLEL_SPEC"] == '1' && multiple_files && !line_specified - "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" - else - "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + command = + begin + line_specified = specs.split.any? { |s| s =~ /\:/ } # Parallel spec can't run specific line + multiple_files = specs.split.count > 1 || specs == "spec" # Only parallelize multiple files + if ENV["PARALLEL_SPEC"] == "1" && multiple_files && !line_specified + "bin/turbo_rspec #{args.join(" ")} #{specs.split.join(" ")}" + else + "bin/rspec #{args.join(" ")} #{specs.split.join(" ")}" + end end - end # launch rspec Dir.chdir(Rails.root) do # rubocop:disable Discourse/NoChdir because this is not part of the app env = { "RAILS_ENV" => "test" } - if specs.split(' ').any? { |s| s =~ /^(.\/)?plugins/ } + if specs.split(" ").any? { |s| s =~ %r{^(./)?plugins} } env["LOAD_PLUGINS"] = "1" puts "Loading plugins while running specs" end - pid = - @mutex.synchronize do - @pid = Process.spawn(env, command) - end + pid = @mutex.synchronize { @pid = Process.spawn(env, command) } _, status = Process.wait2(pid) @@ -51,7 +43,11 @@ module Autospec def abort if pid = @pid - Process.kill("TERM", pid) rescue nil + begin + Process.kill("TERM", pid) + rescue StandardError + nil + end wait_for_done(pid) pid = nil end @@ -66,16 +62,26 @@ module Autospec def wait_for_done(pid) i = 3000 - while (i > 0 && Process.getpgid(pid) rescue nil) + while ( + begin + i > 0 && Process.getpgid(pid) + rescue StandardError + nil + end + ) sleep 0.001 i -= 1 end - if (Process.getpgid(pid) rescue nil) + if ( + begin + Process.getpgid(pid) + rescue StandardError + nil + end + ) STDERR.puts "Terminating rspec #{pid} by force cause it refused graceful termination" Process.kill("KILL", pid) end end - end - end diff --git a/lib/backup_restore.rb b/lib/backup_restore.rb index 291567a6f0..b8c7fa7784 100644 --- a/lib/backup_restore.rb +++ b/lib/backup_restore.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module BackupRestore - - class OperationRunningError < RuntimeError; end + class OperationRunningError < RuntimeError + end VERSION_PREFIX = "v" DUMP_FILE = "dump.sql.gz" @@ -22,9 +22,7 @@ module BackupRestore def self.rollback! raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running? - if can_rollback? - move_tables_between_schemas("backup", "public") - end + move_tables_between_schemas("backup", "public") if can_rollback? end def self.cancel! @@ -58,7 +56,7 @@ module BackupRestore { is_operation_running: is_operation_running?, can_rollback: can_rollback?, - allow_restore: Rails.env.development? || SiteSetting.allow_restore + allow_restore: Rails.env.development? || SiteSetting.allow_restore, } end @@ -133,7 +131,7 @@ module BackupRestore config["backup_port"] || config["port"], config["username"] || username || ENV["USER"] || "postgres", config["password"] || password, - config["database"] + config["database"], ) end @@ -194,7 +192,11 @@ module BackupRestore end def self.backup_tables_count - DB.query_single("SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'").first.to_i + DB + .query_single( + "SELECT COUNT(*) AS count FROM information_schema.tables WHERE table_schema = 'backup'", + ) + .first + .to_i end - end diff --git a/lib/backup_restore/backup_file_handler.rb b/lib/backup_restore/backup_file_handler.rb index 483d6f6473..645fad9f64 100644 --- a/lib/backup_restore/backup_file_handler.rb +++ b/lib/backup_restore/backup_file_handler.rb @@ -69,15 +69,22 @@ module BackupRestore path_transformation = case tar_implementation when :gnu - ['--transform', 's|var/www/discourse/public/uploads/|uploads/|'] + %w[--transform s|var/www/discourse/public/uploads/|uploads/|] when :bsd - ['-s', '|var/www/discourse/public/uploads/|uploads/|'] + %w[-s |var/www/discourse/public/uploads/|uploads/|] end log "Unzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'tar', '--extract', '--gzip', '--file', @archive_path, '--directory', @tmp_directory, - *path_transformation, failure_message: "Failed to decompress archive." + "tar", + "--extract", + "--gzip", + "--file", + @archive_path, + "--directory", + @tmp_directory, + *path_transformation, + failure_message: "Failed to decompress archive.", ) end @@ -86,15 +93,19 @@ module BackupRestore if @is_archive # for compatibility with backups from Discourse v1.5 and below old_dump_path = File.join(@tmp_directory, OLD_DUMP_FILENAME) - File.exist?(old_dump_path) ? old_dump_path : File.join(@tmp_directory, BackupRestore::DUMP_FILE) + if File.exist?(old_dump_path) + old_dump_path + else + File.join(@tmp_directory, BackupRestore::DUMP_FILE) + end else File.join(@tmp_directory, @filename) end - if File.extname(@db_dump_path) == '.gz' + if File.extname(@db_dump_path) == ".gz" log "Extracting dump file..." Compression::Gzip.new.decompress(@tmp_directory, @db_dump_path, available_size) - @db_dump_path.delete_suffix!('.gz') + @db_dump_path.delete_suffix!(".gz") end @db_dump_path @@ -105,17 +116,18 @@ module BackupRestore end def tar_implementation - @tar_version ||= begin - tar_version = Discourse::Utils.execute_command('tar', '--version') + @tar_version ||= + begin + tar_version = Discourse::Utils.execute_command("tar", "--version") - if tar_version.include?("GNU tar") - :gnu - elsif tar_version.include?("bsdtar") - :bsd - else - raise "Unknown tar implementation: #{tar_version}" + if tar_version.include?("GNU tar") + :gnu + elsif tar_version.include?("bsdtar") + :bsd + else + raise "Unknown tar implementation: #{tar_version}" + end end - end end end end diff --git a/lib/backup_restore/backup_store.rb b/lib/backup_restore/backup_store.rb index 65a3acaa95..5afcec27bf 100644 --- a/lib/backup_restore/backup_store.rb +++ b/lib/backup_restore/backup_store.rb @@ -37,9 +37,7 @@ module BackupRestore return unless cleanup_allowed? return if (backup_files = files).size <= SiteSetting.maximum_backups - backup_files[SiteSetting.maximum_backups..-1].each do |file| - delete_file(file.filename) - end + backup_files[SiteSetting.maximum_backups..-1].each { |file| delete_file(file.filename) } reset_cache end @@ -74,7 +72,7 @@ module BackupRestore used_bytes: used_bytes, free_bytes: free_bytes, count: files.size, - last_backup_taken_at: latest_file&.last_modified + last_backup_taken_at: latest_file&.last_modified, } end diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index 83c10911b3..2483f5bb7b 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -4,7 +4,6 @@ require "mini_mime" require "file_store/s3_store" module BackupRestore - class Backuper attr_reader :success @@ -84,7 +83,11 @@ module BackupRestore @dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE) @archive_directory = BackupRestore::LocalBackupStore.base_directory(db: @current_db) filename = @filename_override || "#{get_parameterized_title}-#{@timestamp}" - @archive_basename = File.join(@archive_directory, "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}") + @archive_basename = + File.join( + @archive_directory, + "#{filename}-#{BackupRestore::VERSION_PREFIX}#{BackupRestore.current_version}", + ) @backup_filename = if @with_uploads @@ -119,9 +122,18 @@ module BackupRestore BackupMetadata.delete_all BackupMetadata.create!(name: "base_url", value: Discourse.base_url) BackupMetadata.create!(name: "cdn_url", value: Discourse.asset_host) - BackupMetadata.create!(name: "s3_base_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil) - BackupMetadata.create!(name: "s3_cdn_url", value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil) - BackupMetadata.create!(name: "db_name", value: RailsMultisite::ConnectionManagement.current_db) + BackupMetadata.create!( + name: "s3_base_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_base_url : nil, + ) + BackupMetadata.create!( + name: "s3_cdn_url", + value: SiteSetting.Upload.enable_s3_uploads ? SiteSetting.Upload.s3_cdn_url : nil, + ) + BackupMetadata.create!( + name: "db_name", + value: RailsMultisite::ConnectionManagement.current_db, + ) BackupMetadata.create!(name: "multisite", value: Rails.configuration.multisite) end @@ -132,7 +144,7 @@ module BackupRestore pg_dump_running = true Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) while pg_dump_running message = logs.pop.strip log(message) unless message.blank? @@ -159,23 +171,24 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to pg_dump (if any) - "pg_dump", # the pg_dump command - "--schema=public", # only public schema - "-T public.pg_*", # exclude tables and views whose name starts with "pg_" + [ + password_argument, # pass the password to pg_dump (if any) + "pg_dump", # the pg_dump command + "--schema=public", # only public schema + "-T public.pg_*", # exclude tables and views whose name starts with "pg_" "--file='#{@dump_filename}'", # output to the dump.sql file - "--no-owner", # do not output commands to set ownership of objects - "--no-privileges", # prevent dumping of access privileges - "--verbose", # specifies verbose mode - "--compress=4", # Compression level of 4 - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument, # the username to connect as (if any) - db_conf.database # the name of the database to dump + "--no-owner", # do not output commands to set ownership of objects + "--no-privileges", # prevent dumping of access privileges + "--verbose", # specifies verbose mode + "--compress=4", # Compression level of 4 + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) + db_conf.database, # the name of the database to dump ].join(" ") end @@ -185,8 +198,10 @@ module BackupRestore archive_filename = File.join(@archive_directory, @backup_filename) Discourse::Utils.execute_command( - 'mv', @dump_filename, archive_filename, - failure_message: "Failed to move database dump file." + "mv", + @dump_filename, + archive_filename, + failure_message: "Failed to move database dump file.", ) remove_tmp_directory @@ -198,17 +213,29 @@ module BackupRestore tar_filename = "#{@archive_basename}.tar" log "Making sure archive does not already exist..." - Discourse::Utils.execute_command('rm', '-f', tar_filename) - Discourse::Utils.execute_command('rm', '-f', "#{tar_filename}.gz") + Discourse::Utils.execute_command("rm", "-f", tar_filename) + Discourse::Utils.execute_command("rm", "-f", "#{tar_filename}.gz") log "Creating empty archive..." - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, '--files-from', '/dev/null') + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + "--files-from", + "/dev/null", + ) log "Archiving data dump..." Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', '--file', tar_filename, File.basename(@dump_filename), + "tar", + "--append", + "--dereference", + "--file", + tar_filename, + File.basename(@dump_filename), failure_message: "Failed to archive data dump.", - chdir: File.dirname(@dump_filename) + chdir: File.dirname(@dump_filename), ) add_local_uploads_to_archive(tar_filename) @@ -218,8 +245,10 @@ module BackupRestore log "Gzipping archive, this may take a while..." Discourse::Utils.execute_command( - 'gzip', "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", tar_filename, - failure_message: "Failed to gzip archive." + "gzip", + "-#{SiteSetting.backup_gzip_compression_level_for_uploads}", + tar_filename, + failure_message: "Failed to gzip archive.", ) end @@ -244,14 +273,21 @@ module BackupRestore if SiteSetting.include_thumbnails_in_backups exclude_optimized = "" else - optimized_path = File.join(upload_directory, 'optimized') + optimized_path = File.join(upload_directory, "optimized") exclude_optimized = "--exclude=#{optimized_path}" end Discourse::Utils.execute_command( - 'tar', '--append', '--dereference', exclude_optimized, '--file', tar_filename, upload_directory, - failure_message: "Failed to archive uploads.", success_status_codes: [0, 1], - chdir: File.join(Rails.root, "public") + "tar", + "--append", + "--dereference", + exclude_optimized, + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to archive uploads.", + success_status_codes: [0, 1], + chdir: File.join(Rails.root, "public"), ) else log "No local uploads found. Skipping archiving of local uploads..." @@ -287,9 +323,14 @@ module BackupRestore log "Appending uploads to archive..." Discourse::Utils.execute_command( - 'tar', '--append', '--file', tar_filename, upload_directory, - failure_message: "Failed to append uploads to archive.", success_status_codes: [0, 1], - chdir: @tmp_directory + "tar", + "--append", + "--file", + tar_filename, + upload_directory, + failure_message: "Failed to append uploads to archive.", + success_status_codes: [0, 1], + chdir: @tmp_directory, ) log "No uploads found on S3. Skipping archiving of uploads stored on S3..." if count == 0 @@ -327,9 +368,7 @@ module BackupRestore logs = Discourse::Utils.logs_markdown(@logs, user: @user) post = SystemMessage.create_from_system_user(@user, status, logs: logs) - if @user.id == Discourse::SYSTEM_USER_ID - post.topic.invite_group(@user, Group[:admins]) - end + post.topic.invite_group(@user, Group[:admins]) if @user.id == Discourse::SYSTEM_USER_ID rescue => ex log "Something went wrong while notifying user.", ex end @@ -399,7 +438,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "backup", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) @@ -416,5 +460,4 @@ module BackupRestore end end end - end diff --git a/lib/backup_restore/database_restorer.rb b/lib/backup_restore/database_restorer.rb index ae3ac7deaa..2a3bc6985c 100644 --- a/lib/backup_restore/database_restorer.rb +++ b/lib/backup_restore/database_restorer.rb @@ -46,9 +46,7 @@ module BackupRestore end def self.drop_backup_schema - if backup_schema_dropable? - ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) - end + ActiveRecord::Base.connection.drop_schema(BACKUP_SCHEMA) if backup_schema_dropable? end def self.core_migration_files @@ -65,13 +63,14 @@ module BackupRestore last_line = nil psql_running = true - log_thread = Thread.new do - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) - while psql_running || !logs.empty? - message = logs.pop.strip - log(message) if message.present? + log_thread = + Thread.new do + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) + while psql_running || !logs.empty? + message = logs.pop.strip + log(message) if message.present? + end end - end IO.popen(restore_dump_command) do |pipe| begin @@ -89,7 +88,9 @@ module BackupRestore logs << "" log_thread.join - raise DatabaseRestoreError.new("psql failed: #{last_line}") if Process.last_status&.exitstatus != 0 + if Process.last_status&.exitstatus != 0 + raise DatabaseRestoreError.new("psql failed: #{last_line}") + end end # Removes unwanted SQL added by certain versions of pg_dump and modifies @@ -99,7 +100,7 @@ module BackupRestore "DROP SCHEMA", # Discourse <= v1.5 "CREATE SCHEMA", # PostgreSQL 11+ "COMMENT ON SCHEMA", # PostgreSQL 11+ - "SET default_table_access_method" # PostgreSQL 12 + "SET default_table_access_method", # PostgreSQL 12 ].join("|") command = "sed -E '/^(#{unwanted_sql})/d' #{@db_dump_path}" @@ -117,18 +118,19 @@ module BackupRestore db_conf = BackupRestore.database_configuration password_argument = "PGPASSWORD='#{db_conf.password}'" if db_conf.password.present? - host_argument = "--host=#{db_conf.host}" if db_conf.host.present? - port_argument = "--port=#{db_conf.port}" if db_conf.port.present? - username_argument = "--username=#{db_conf.username}" if db_conf.username.present? + host_argument = "--host=#{db_conf.host}" if db_conf.host.present? + port_argument = "--port=#{db_conf.port}" if db_conf.port.present? + username_argument = "--username=#{db_conf.username}" if db_conf.username.present? - [ password_argument, # pass the password to psql (if any) - "psql", # the psql command + [ + password_argument, # pass the password to psql (if any) + "psql", # the psql command "--dbname='#{db_conf.database}'", # connect to database *dbname* - "--single-transaction", # all or nothing (also runs COPY commands faster) - "--variable=ON_ERROR_STOP=1", # stop on first error - host_argument, # the hostname to connect to (if any) - port_argument, # the port to connect to (if any) - username_argument # the username to connect as (if any) + "--single-transaction", # all or nothing (also runs COPY commands faster) + "--variable=ON_ERROR_STOP=1", # stop on first error + host_argument, # the hostname to connect to (if any) + port_argument, # the port to connect to (if any) + username_argument, # the username to connect as (if any) ].compact.join(" ") end @@ -136,21 +138,22 @@ module BackupRestore log "Migrating the database..." log Discourse::Utils.execute_command( - { - "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", - "SKIP_OPTIMIZE_ICONS" => "1", - "DISABLE_TRANSLATION_OVERRIDES" => "1" - }, - "rake", "db:migrate", - failure_message: "Failed to migrate database.", - chdir: Rails.root - ) + { + "SKIP_POST_DEPLOYMENT_MIGRATIONS" => "0", + "SKIP_OPTIMIZE_ICONS" => "1", + "DISABLE_TRANSLATION_OVERRIDES" => "1", + }, + "rake", + "db:migrate", + failure_message: "Failed to migrate database.", + chdir: Rails.root, + ) end def reconnect_database log "Reconnecting to the database..." - RailsMultisite::ConnectionManagement::reload if RailsMultisite::ConnectionManagement::instance - RailsMultisite::ConnectionManagement::establish_connection(db: @current_db) + RailsMultisite::ConnectionManagement.reload if RailsMultisite::ConnectionManagement.instance + RailsMultisite::ConnectionManagement.establish_connection(db: @current_db) end def create_missing_discourse_functions @@ -179,10 +182,12 @@ module BackupRestore end end - existing_function_names = Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } + existing_function_names = + Migration::BaseDropper.existing_discourse_function_names.map { |name| "#{name}()" } all_readonly_table_columns.each do |table_name, column_name| - function_name = Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) + function_name = + Migration::BaseDropper.readonly_function_name(table_name, column_name, with_schema: false) if !existing_function_names.include?(function_name) Migration::BaseDropper.create_readonly_function(table_name, column_name) diff --git a/lib/backup_restore/local_backup_store.rb b/lib/backup_restore/local_backup_store.rb index fd7f16b9f6..77ccfcce09 100644 --- a/lib/backup_restore/local_backup_store.rb +++ b/lib/backup_restore/local_backup_store.rb @@ -12,7 +12,12 @@ module BackupRestore end def self.chunk_path(identifier, filename, chunk_number) - File.join(LocalBackupStore.base_directory, "tmp", identifier, "#{filename}.part#{chunk_number}") + File.join( + LocalBackupStore.base_directory, + "tmp", + identifier, + "#{filename}.part#{chunk_number}", + ) end def initialize(opts = {}) @@ -39,7 +44,7 @@ module BackupRestore def download_file(filename, destination, failure_message = "") path = path_from_filename(filename) - Discourse::Utils.execute_command('cp', path, destination, failure_message: failure_message) + Discourse::Utils.execute_command("cp", path, destination, failure_message: failure_message) end private @@ -59,7 +64,7 @@ module BackupRestore filename: File.basename(path), size: File.size(path), last_modified: File.mtime(path).utc, - source: include_download_source ? path : nil + source: include_download_source ? path : nil, ) end diff --git a/lib/backup_restore/logger.rb b/lib/backup_restore/logger.rb index fe777d389b..f9c0eab642 100644 --- a/lib/backup_restore/logger.rb +++ b/lib/backup_restore/logger.rb @@ -32,7 +32,12 @@ module BackupRestore def publish_log(message, timestamp) return unless @publish_to_message_bus data = { timestamp: timestamp, operation: "restore", message: message } - MessageBus.publish(BackupRestore::LOGS_CHANNEL, data, user_ids: [@user_id], client_ids: [@client_id]) + MessageBus.publish( + BackupRestore::LOGS_CHANNEL, + data, + user_ids: [@user_id], + client_ids: [@client_id], + ) end def save_log(message, timestamp) diff --git a/lib/backup_restore/meta_data_handler.rb b/lib/backup_restore/meta_data_handler.rb index 02bc0e98bf..0996d8866b 100644 --- a/lib/backup_restore/meta_data_handler.rb +++ b/lib/backup_restore/meta_data_handler.rb @@ -28,8 +28,10 @@ module BackupRestore log " Restored version: #{metadata[:version]}" if metadata[:version] > @current_version - raise MigrationRequiredError.new("You're trying to restore a more recent version of the schema. " \ - "You should migrate first!") + raise MigrationRequiredError.new( + "You're trying to restore a more recent version of the schema. " \ + "You should migrate first!", + ) end metadata diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index 00bedab241..8caf5dafec 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -65,8 +65,8 @@ module BackupRestore after_restore_hook rescue Compression::Strategy::ExtractFailed - log 'ERROR: The uncompressed file is too big. Consider increasing the hidden ' \ - '"decompressed_backup_max_file_size_mb" setting.' + log "ERROR: The uncompressed file is too big. Consider increasing the hidden " \ + '"decompressed_backup_max_file_size_mb" setting.' @database_restorer.rollback rescue SystemExit log "Restore process was cancelled!" @@ -118,10 +118,10 @@ module BackupRestore DiscourseEvent.trigger(:site_settings_restored) - if @disable_emails && SiteSetting.disable_emails == 'no' + if @disable_emails && SiteSetting.disable_emails == "no" log "Disabling outgoing emails for non-staff users..." user = User.find_by_email(@user_info[:email]) || Discourse.system_user - SiteSetting.set_and_log(:disable_emails, 'non-staff', user) + SiteSetting.set_and_log(:disable_emails, "non-staff", user) end end @@ -152,7 +152,7 @@ module BackupRestore post = SystemMessage.create_from_system_user(user, status, logs: logs) else log "Could not send notification to '#{@user_info[:username]}' " \ - "(#{@user_info[:email]}), because the user does not exist." + "(#{@user_info[:email]}), because the user does not exist." end rescue => ex log "Something went wrong while notifying user.", ex diff --git a/lib/backup_restore/s3_backup_store.rb b/lib/backup_restore/s3_backup_store.rb index 591eb2ed06..10e2798642 100644 --- a/lib/backup_restore/s3_backup_store.rb +++ b/lib/backup_restore/s3_backup_store.rb @@ -4,8 +4,11 @@ module BackupRestore class S3BackupStore < BackupStore UPLOAD_URL_EXPIRES_AFTER_SECONDS ||= 6.hours.to_i - delegate :abort_multipart, :presign_multipart_part, :list_multipart_parts, - :complete_multipart, to: :s3_helper + delegate :abort_multipart, + :presign_multipart_part, + :list_multipart_parts, + :complete_multipart, + to: :s3_helper def initialize(opts = {}) @s3_options = S3Helper.s3_options(SiteSetting) @@ -13,7 +16,7 @@ module BackupRestore end def s3_helper - @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, '', @s3_options.clone) + @s3_helper ||= S3Helper.new(s3_bucket_name_with_prefix, "", @s3_options.clone) end def remote? @@ -57,11 +60,17 @@ module BackupRestore presigned_url(obj, :put, UPLOAD_URL_EXPIRES_AFTER_SECONDS) rescue Aws::Errors::ServiceError => e - Rails.logger.warn("Failed to generate upload URL for S3: #{e.message.presence || e.class.name}") + Rails.logger.warn( + "Failed to generate upload URL for S3: #{e.message.presence || e.class.name}", + ) raise StorageError.new(e.message.presence || e.class.name) end - def signed_url_for_temporary_upload(file_name, expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, metadata: {}) + def signed_url_for_temporary_upload( + file_name, + expires_in: S3Helper::UPLOAD_URL_EXPIRES_AFTER_SECONDS, + metadata: {} + ) obj = object_from_path(file_name) raise BackupFileExists.new if obj.exists? key = temporary_upload_path(file_name) @@ -71,8 +80,8 @@ module BackupRestore expires_in: expires_in, opts: { metadata: metadata, - acl: "private" - } + acl: "private", + }, ) end @@ -84,7 +93,7 @@ module BackupRestore folder_prefix = s3_helper.s3_bucket_folder_path.nil? ? "" : s3_helper.s3_bucket_folder_path if Rails.env.test? - folder_prefix = File.join(folder_prefix, "test_#{ENV['TEST_ENV_NUMBER'].presence || '0'}") + folder_prefix = File.join(folder_prefix, "test_#{ENV["TEST_ENV_NUMBER"].presence || "0"}") end folder_prefix @@ -105,7 +114,10 @@ module BackupRestore s3_helper.copy( existing_external_upload_key, File.join(s3_helper.s3_bucket_folder_path, original_filename), - options: { acl: "private", apply_metadata_to_destination: true } + options: { + acl: "private", + apply_metadata_to_destination: true, + }, ) s3_helper.delete_object(existing_external_upload_key) end @@ -120,9 +132,7 @@ module BackupRestore objects = [] s3_helper.list.each do |obj| - if obj.key.match?(file_regex) - objects << create_file_from_object(obj) - end + objects << create_file_from_object(obj) if obj.key.match?(file_regex) end objects @@ -137,7 +147,7 @@ module BackupRestore filename: File.basename(obj.key), size: obj.size, last_modified: obj.last_modified, - source: include_download_source ? presigned_url(obj, :get, expires) : nil + source: include_download_source ? presigned_url(obj, :get, expires) : nil, ) end @@ -154,16 +164,17 @@ module BackupRestore end def file_regex - @file_regex ||= begin - path = s3_helper.s3_bucket_folder_path || "" + @file_regex ||= + begin + path = s3_helper.s3_bucket_folder_path || "" - if path.present? - path = "#{path}/" unless path.end_with?("/") - path = Regexp.quote(path) + if path.present? + path = "#{path}/" unless path.end_with?("/") + path = Regexp.quote(path) + end + + %r{^#{path}[^/]*\.t?gz$}i end - - /^#{path}[^\/]*\.t?gz$/i - end end def free_bytes diff --git a/lib/backup_restore/system_interface.rb b/lib/backup_restore/system_interface.rb index c0d209e538..31bd4eb944 100644 --- a/lib/backup_restore/system_interface.rb +++ b/lib/backup_restore/system_interface.rb @@ -98,9 +98,7 @@ module BackupRestore def flush_redis redis = Discourse.redis - redis.scan_each(match: "*") do |key| - redis.del(key) unless key == SidekiqPauser::PAUSED_KEY - end + redis.scan_each(match: "*") { |key| redis.del(key) unless key == SidekiqPauser::PAUSED_KEY } end def clear_sidekiq_queues diff --git a/lib/backup_restore/uploads_restorer.rb b/lib/backup_restore/uploads_restorer.rb index 7409c38f56..8b6c13e17d 100644 --- a/lib/backup_restore/uploads_restorer.rb +++ b/lib/backup_restore/uploads_restorer.rb @@ -11,11 +11,12 @@ module BackupRestore def self.s3_regex_string(s3_base_url) clean_url = s3_base_url.sub(S3_ENDPOINT_REGEX, ".s3.amazonaws.com") - regex_string = clean_url - .split(".s3.amazonaws.com") - .map { |s| Regexp.escape(s) } - .insert(1, S3_ENDPOINT_REGEX.source) - .join("") + regex_string = + clean_url + .split(".s3.amazonaws.com") + .map { |s| Regexp.escape(s) } + .insert(1, S3_ENDPOINT_REGEX.source) + .join("") [regex_string, clean_url] end @@ -25,12 +26,16 @@ module BackupRestore end def restore(tmp_directory) - upload_directories = Dir.glob(File.join(tmp_directory, "uploads", "*")) - .reject { |path| File.basename(path).start_with?("PaxHeaders") } + upload_directories = + Dir + .glob(File.join(tmp_directory, "uploads", "*")) + .reject { |path| File.basename(path).start_with?("PaxHeaders") } if upload_directories.count > 1 - raise UploadsRestoreError.new("Could not find uploads, because the uploads " \ - "directory contains multiple folders.") + raise UploadsRestoreError.new( + "Could not find uploads, because the uploads " \ + "directory contains multiple folders.", + ) end @tmp_uploads_path = upload_directories.first @@ -55,7 +60,9 @@ module BackupRestore if !store.respond_to?(:copy_from) # a FileStore implementation from a plugin might not support this method, so raise a helpful error store_name = Discourse.store.class.name - raise UploadsRestoreError.new("The current file store (#{store_name}) does not support restoring uploads.") + raise UploadsRestoreError.new( + "The current file store (#{store_name}) does not support restoring uploads.", + ) end log "Restoring uploads, this may take a while..." @@ -89,13 +96,17 @@ module BackupRestore remap(old_base_url, Discourse.base_url) end - current_s3_base_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil - if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && old_s3_base_url != current_s3_base_url + current_s3_base_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_base_url : nil + if (old_s3_base_url = BackupMetadata.value_for("s3_base_url")) && + old_s3_base_url != current_s3_base_url remap_s3("#{old_s3_base_url}/", uploads_folder) end - current_s3_cdn_url = SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil - if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && old_s3_cdn_url != current_s3_cdn_url + current_s3_cdn_url = + SiteSetting::Upload.enable_s3_uploads ? SiteSetting::Upload.s3_cdn_url : nil + if (old_s3_cdn_url = BackupMetadata.value_for("s3_cdn_url")) && + old_s3_cdn_url != current_s3_cdn_url base_url = current_s3_cdn_url || Discourse.base_url remap("#{old_s3_cdn_url}/", UrlHelper.schemaless("#{base_url}#{uploads_folder}")) @@ -113,10 +124,7 @@ module BackupRestore remap(old_host, new_host) if old_host != new_host end - if @previous_db_name != @current_db_name - remap("/uploads/#{@previous_db_name}/", upload_path) - end - + remap("/uploads/#{@previous_db_name}/", upload_path) if @previous_db_name != @current_db_name rescue => ex log "Something went wrong while remapping uploads.", ex end @@ -130,7 +138,12 @@ module BackupRestore if old_s3_base_url.include?("amazonaws.com") from_regex, from_clean_url = self.class.s3_regex_string(old_s3_base_url) log "Remapping with regex from '#{from_clean_url}' to '#{uploads_folder}'" - DbHelper.regexp_replace(from_regex, uploads_folder, verbose: true, excluded_tables: ["backup_metadata"]) + DbHelper.regexp_replace( + from_regex, + uploads_folder, + verbose: true, + excluded_tables: ["backup_metadata"], + ) else remap(old_s3_base_url, uploads_folder) end @@ -141,13 +154,15 @@ module BackupRestore DB.exec("TRUNCATE TABLE optimized_images") SiteIconManager.ensure_optimized! - User.where("uploaded_avatar_id IS NOT NULL").find_each do |user| - Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) - end + User + .where("uploaded_avatar_id IS NOT NULL") + .find_each do |user| + Jobs.enqueue(:create_avatar_thumbnails, upload_id: user.uploaded_avatar_id) + end end def rebake_posts_with_uploads - log 'Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed.' + log "Posts will be rebaked by a background job in sidekiq. You will see missing images until that has completed." log 'You can expedite the process by manually running "rake posts:rebake_uncooked_posts"' DB.exec(<<~SQL) diff --git a/lib/badge_queries.rb b/lib/badge_queries.rb index 0f8a4c4832..4eda809873 100644 --- a/lib/badge_queries.rb +++ b/lib/badge_queries.rb @@ -173,7 +173,7 @@ module BadgeQueries <<~SQL SELECT p.user_id, p.id post_id, current_timestamp granted_at FROM badge_posts p - WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1" } AND p.like_count >= #{count.to_i} AND + WHERE #{is_topic ? "p.post_number = 1" : "p.post_number > 1"} AND p.like_count >= #{count.to_i} AND (:backfill OR p.id IN (:post_ids) ) SQL end @@ -271,5 +271,4 @@ module BadgeQueries WHERE "rank" = 1 SQL end - end diff --git a/lib/bookmark_query.rb b/lib/bookmark_query.rb index f62ce591fa..3a54a19580 100644 --- a/lib/bookmark_query.rb +++ b/lib/bookmark_query.rb @@ -11,9 +11,7 @@ class BookmarkQuery def self.preload(bookmarks, object) preload_polymorphic_associations(bookmarks, object.guardian) - if @preload - @preload.each { |preload| preload.call(bookmarks, object) } - end + @preload.each { |preload| preload.call(bookmarks, object) } if @preload end # These polymorphic associations are loaded to make the UserBookmarkListSerializer's @@ -42,24 +40,27 @@ class BookmarkQuery ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil - queries = Bookmark.registered_bookmarkables.map do |bookmarkable| - interim_results = bookmarkable.perform_list_query(@user, @guardian) + queries = + Bookmark + .registered_bookmarkables + .map do |bookmarkable| + interim_results = bookmarkable.perform_list_query(@user, @guardian) - # this could occur if there is some security reason that the user cannot - # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark - # on a topic and that topic was moved into a private category - next if interim_results.blank? + # this could occur if there is some security reason that the user cannot + # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark + # on a topic and that topic was moved into a private category + next if interim_results.blank? - if search_term.present? - interim_results = bookmarkable.perform_search_query( - interim_results, search_term_wildcard, ts_query - ) - end + if search_term.present? + interim_results = + bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query) + end - # this is purely to make the query easy to read and debug, otherwise it's - # all mashed up into a massive ball in MiniProfiler :) - "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" - end.compact + # this is purely to make the query easy to read and debug, otherwise it's + # all mashed up into a massive ball in MiniProfiler :) + "---- #{bookmarkable.model.to_s} bookmarkable ---\n\n #{interim_results.to_sql}" + end + .compact # same for interim results being blank, the user might have been locked out # from all their various bookmarks, in which case they will see nothing and @@ -68,17 +69,16 @@ class BookmarkQuery union_sql = queries.join("\n\nUNION\n\n") results = Bookmark.select("bookmarks.*").from("(\n\n#{union_sql}\n\n) as bookmarks") - results = results.order( - "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), + results = + results.order( + "(CASE WHEN bookmarks.pinned THEN 0 ELSE 1 END), bookmarks.reminder_at ASC, - bookmarks.updated_at DESC" - ) + bookmarks.updated_at DESC", + ) @count = results.count - if @page.positive? - results = results.offset(@page * @params[:per_page]) - end + results = results.offset(@page * @params[:per_page]) if @page.positive? if updated_results = blk&.call(results) results = updated_results diff --git a/lib/bookmark_reminder_notification_handler.rb b/lib/bookmark_reminder_notification_handler.rb index 13f5b2966e..247cc4637d 100644 --- a/lib/bookmark_reminder_notification_handler.rb +++ b/lib/bookmark_reminder_notification_handler.rb @@ -28,12 +28,10 @@ class BookmarkReminderNotificationHandler def clear_reminder Rails.logger.debug( - "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}" + "Clearing bookmark reminder for bookmark_id #{bookmark.id}. reminder at: #{bookmark.reminder_at}", ) - if bookmark.auto_clear_reminder_when_reminder_sent? - bookmark.reminder_at = nil - end + bookmark.reminder_at = nil if bookmark.auto_clear_reminder_when_reminder_sent? bookmark.clear_reminder! end diff --git a/lib/browser_detection.rb b/lib/browser_detection.rb index e2f8d46a88..7cfbaffe3b 100644 --- a/lib/browser_detection.rb +++ b/lib/browser_detection.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module BrowserDetection - def self.browser(user_agent) case user_agent when /Edg/i @@ -66,5 +65,4 @@ module BrowserDetection :unknown end end - end diff --git a/lib/cache.rb b/lib/cache.rb index ca814c9e2b..296519b17e 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -15,12 +15,11 @@ # this makes it harder to reason about the API class Cache - # nothing is cached for longer than 1 day EVER # there is no reason to have data older than this clogging redis # it is dangerous cause if we rename keys we will be stuck with # pointless data - MAX_CACHE_AGE = 1.day unless defined? MAX_CACHE_AGE + MAX_CACHE_AGE = 1.day unless defined?(MAX_CACHE_AGE) attr_reader :namespace @@ -47,9 +46,7 @@ class Cache end def clear - keys.each do |k| - redis.del(k) - end + keys.each { |k| redis.del(k) } end def normalize_key(key) @@ -80,9 +77,7 @@ class Cache key = normalize_key(name) raw = nil - if !force - raw = redis.get(key) - end + raw = redis.get(key) if !force if raw begin @@ -96,7 +91,8 @@ class Cache val end elsif force - raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block." + raise ArgumentError, + "Missing block: Calling `Cache#fetch` with `force: true` requires a block." else read(name) end @@ -105,7 +101,7 @@ class Cache protected def log_first_exception(e) - if !defined? @logged_a_warning + if !defined?(@logged_a_warning) @logged_a_warning = true Discourse.warn_exception(e, "Corrupt cache... skipping entry for key #{key}") end @@ -129,5 +125,4 @@ class Cache redis.setex(key, expiry, dumped) true end - end diff --git a/lib/canonical_url.rb b/lib/canonical_url.rb index df66c37a6c..bc9b6e2d10 100644 --- a/lib/canonical_url.rb +++ b/lib/canonical_url.rb @@ -2,7 +2,7 @@ module CanonicalURL module ControllerExtensions - ALLOWED_CANONICAL_PARAMS = %w(page) + ALLOWED_CANONICAL_PARAMS = %w[page] def canonical_url(url_for_options = {}) case url_for_options @@ -14,14 +14,15 @@ module CanonicalURL end def default_canonical - @default_canonical ||= begin - canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" - allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } - if allowed_params.present? - canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + @default_canonical ||= + begin + canonical = +"#{Discourse.base_url_no_prefix}#{request.path}" + allowed_params = params.select { |key| ALLOWED_CANONICAL_PARAMS.include?(key) } + if allowed_params.present? + canonical << "?#{allowed_params.keys.zip(allowed_params.values).map { |key, value| "#{key}=#{value}" }.join("&")}" + end + canonical end - canonical - end end def self.included(base) @@ -31,7 +32,7 @@ module CanonicalURL module Helpers def canonical_link_tag(url = nil) - tag('link', rel: 'canonical', href: url || @canonical_url || default_canonical) + tag("link", rel: "canonical", href: url || @canonical_url || default_canonical) end end end diff --git a/lib/category_badge.rb b/lib/category_badge.rb index fdfd8035c5..9cf422e588 100644 --- a/lib/category_badge.rb +++ b/lib/category_badge.rb @@ -1,23 +1,26 @@ # frozen_string_literal: true module CategoryBadge - def self.category_stripe(color, classes) - style = color ? "style='background-color: ##{color};'" : '' + style = color ? "style='background-color: ##{color};'" : "" "" end - def self.inline_category_stripe(color, styles = '', insert_blank = false) - "#{insert_blank ? ' ' : ''}" + def self.inline_category_stripe(color, styles = "", insert_blank = false) + "#{insert_blank ? " " : ""}" end def self.inline_badge_wrapper_style(category) style = case (SiteSetting.category_style || :box).to_sym - when :bar then 'line-height: 1.25; margin-right: 5px;' - when :box then "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" - when :bullet then 'line-height: 1; margin-right: 10px;' - when :none then '' + when :bar + "line-height: 1.25; margin-right: 5px;" + when :box + "background-color:##{category.color}; line-height: 1.5; margin-top: 5px; margin-right: 5px;" + when :bullet + "line-height: 1; margin-right: 10px;" + when :none + "" end " style='font-size: 0.857em; white-space: nowrap; display: inline-block; position: relative; #{style}'" @@ -34,73 +37,88 @@ module CategoryBadge extra_classes = "#{opts[:extra_classes]} #{SiteSetting.category_style}" - result = +'' + result = +"" # parent span unless category.parent_category_id.nil? || opts[:hide_parent] parent_category = Category.find_by(id: category.parent_category_id) - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 1px;', true) - when :box - inline_category_stripe(parent_category.color, 'display: inline-block; padding: 0 1px;', true) - when :bullet - inline_category_stripe(parent_category.color, 'display: inline-block; width: 5px; height: 10px; line-height: 1;') - when :none - '' - end - else - category_stripe(parent_category.color, 'badge-category-parent-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 1px;", + true, + ) + when :box + inline_category_stripe( + parent_category.color, + "display: inline-block; padding: 0 1px;", + true, + ) + when :bullet + inline_category_stripe( + parent_category.color, + "display: inline-block; width: 5px; height: 10px; line-height: 1;", + ) + when :none + "" end + else + category_stripe(parent_category.color, "badge-category-parent-bg") + end end # sub parent or main category span - result << - if opts[:inline_style] - case (SiteSetting.category_style || :box).to_sym - when :bar - inline_category_stripe(category.color, 'display: inline-block; padding: 1px;', true) - when :box - '' - when :bullet - inline_category_stripe(category.color, "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;") - when :none - '' - end - else - category_stripe(category.color, 'badge-category-bg') + result << if opts[:inline_style] + case (SiteSetting.category_style || :box).to_sym + when :bar + inline_category_stripe(category.color, "display: inline-block; padding: 1px;", true) + when :box + "" + when :bullet + inline_category_stripe( + category.color, + "display: inline-block; width: #{category.parent_category_id.nil? ? 10 : 5}px; height: 10px;", + ) + when :none + "" end + else + category_stripe(category.color, "badge-category-bg") + end # category name - class_names = 'badge-category clear-badge' - description = category.description_text ? "title='#{category.description_text}'" : '' - category_url = opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url + class_names = "badge-category clear-badge" + description = category.description_text ? "title='#{category.description_text}'" : "" + category_url = + opts[:absolute_url] ? "#{Discourse.base_url_no_prefix}#{category.url}" : category.url extra_span_classes = if opts[:inline_style] case (SiteSetting.category_style || :box).to_sym when :bar - 'color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;' + "color: #222222; padding: 3px; vertical-align: text-top; margin-top: -3px; display: inline-block;" when :box "color: ##{category.text_color}; padding: 0 5px;" when :bullet - 'color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;' + "color: #222222; vertical-align: text-top; line-height: 1; margin-left: 4px; padding-left: 2px; display: inline;" when :none - '' - end + 'max-width: 150px; overflow: hidden; text-overflow: ellipsis;' + "" + end + "max-width: 150px; overflow: hidden; text-overflow: ellipsis;" elsif (SiteSetting.category_style).to_sym == :box "color: ##{category.text_color}" else - '' + "" end result << "" - result << ERB::Util.html_escape(category.name) << '' + result << ERB::Util.html_escape(category.name) << "" - result = "#{result}" + result = + "#{result}" result.html_safe end diff --git a/lib/chrome_installed_checker.rb b/lib/chrome_installed_checker.rb index 0576eb7401..2f8c2824cc 100644 --- a/lib/chrome_installed_checker.rb +++ b/lib/chrome_installed_checker.rb @@ -3,13 +3,17 @@ require "rbconfig" class ChromeInstalledChecker - class ChromeError < StandardError; end - class ChromeVersionError < ChromeError; end - class ChromeNotInstalled < ChromeError; end - class ChromeVersionTooLow < ChromeError; end + class ChromeError < StandardError + end + class ChromeVersionError < ChromeError + end + class ChromeNotInstalled < ChromeError + end + class ChromeVersionTooLow < ChromeError + end def self.run - if RbConfig::CONFIG['host_os'][/darwin|mac os/] + if RbConfig::CONFIG["host_os"][/darwin|mac os/] binary = "/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome" elsif system("command -v google-chrome-stable >/dev/null;") binary = "google-chrome-stable" @@ -18,15 +22,15 @@ class ChromeInstalledChecker binary ||= "chromium" if system("command -v chromium >/dev/null;") if !binary - raise ChromeNotInstalled.new("Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html") + raise ChromeNotInstalled.new( + "Chrome is not installed. Download from https://www.google.com/chrome/browser/desktop/index.html", + ) end version = `\"#{binary}\" --version` version_match = version.match(/[\d\.]+/) - if !version_match - raise ChromeError.new("Can't get the #{binary} version") - end + raise ChromeError.new("Can't get the #{binary} version") if !version_match if Gem::Version.new(version_match[0]) < Gem::Version.new("59") raise ChromeVersionTooLow.new("Chrome 59 or higher is required") diff --git a/lib/comment_migration.rb b/lib/comment_migration.rb index bcfcdfd10e..94062378c2 100644 --- a/lib/comment_migration.rb +++ b/lib/comment_migration.rb @@ -28,24 +28,27 @@ class CommentMigration < ActiveRecord::Migration[4.2] end def down - replace_nils(comments_up).deep_merge(comments_down).each do |table| - table[1].each do |column| - table_name = table[0] - column_name = column[0] - comment = column[1] + replace_nils(comments_up) + .deep_merge(comments_down) + .each do |table| + table[1].each do |column| + table_name = table[0] + column_name = column[0] + comment = column[1] - if column_name == :_table - DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment - puts " COMMENT ON TABLE #{table_name}" - else - DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment - puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + if column_name == :_table + DB.exec "COMMENT ON TABLE #{table_name} IS ?", comment + puts " COMMENT ON TABLE #{table_name}" + else + DB.exec "COMMENT ON COLUMN #{table_name}.#{column_name} IS ?", comment + puts " COMMENT ON COLUMN #{table_name}.#{column_name}" + end end end - end end private + def replace_nils(hash) hash.each do |key, value| if Hash === value diff --git a/lib/common_passwords.rb b/lib/common_passwords.rb index 901a007b01..5b4aec28e8 100644 --- a/lib/common_passwords.rb +++ b/lib/common_passwords.rb @@ -12,9 +12,8 @@ # Discourse.redis.without_namespace.del CommonPasswords::LIST_KEY class CommonPasswords - - PASSWORD_FILE = File.join(Rails.root, 'lib', 'common_passwords', '10-char-common-passwords.txt') - LIST_KEY = 'discourse-common-passwords' + PASSWORD_FILE = File.join(Rails.root, "lib", "common_passwords", "10-char-common-passwords.txt") + LIST_KEY = "discourse-common-passwords" @mutex = Mutex.new @@ -32,9 +31,7 @@ class CommonPasswords end def self.password_list - @mutex.synchronize do - load_passwords unless redis.scard(LIST_KEY) > 0 - end + @mutex.synchronize { load_passwords unless redis.scard(LIST_KEY) > 0 } RedisPasswordList.new end @@ -49,5 +46,4 @@ class CommonPasswords # tolerate this so we don't block signups Rails.logger.error "Common passwords file #{PASSWORD_FILE} is not found! Common password checking is skipped." end - end diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 510048d1db..878727f20c 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class ComposerMessagesFinder - def initialize(user, details) @user = user @details = details @@ -29,26 +28,30 @@ class ComposerMessagesFinder if creating_topic? count = @user.created_topic_count - education_key = 'education.new-topic' + education_key = "education.new-topic" else count = @user.post_count - education_key = 'education.new-reply' + education_key = "education.new-reply" end if count < SiteSetting.educate_until_posts - return { - id: 'education', - templateName: 'education', - wait_for_typing: true, - body: PrettyText.cook( - I18n.t( - education_key, - education_posts_text: I18n.t('education.until_posts', count: SiteSetting.educate_until_posts), - site_name: SiteSetting.title, - base_path: Discourse.base_path - ) - ) - } + return( + { + id: "education", + templateName: "education", + wait_for_typing: true, + body: + PrettyText.cook( + I18n.t( + education_key, + education_posts_text: + I18n.t("education.until_posts", count: SiteSetting.educate_until_posts), + site_name: SiteSetting.title, + base_path: Discourse.base_path, + ), + ), + } + ) end nil @@ -59,35 +62,55 @@ class ComposerMessagesFinder return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id]) { - id: 'too_many_replies', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) + id: "too_many_replies", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.too_many_replies", + newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic, + ), + ), } end # Should a user be contacted to update their avatar? def check_avatar_notification - # A user has to be basic at least to be considered for an avatar notification return unless @user.has_trust_level?(TrustLevel[1]) # We don't notify users who have avatars or who have been notified already. - return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) + return + end # Do not notify user if any of the following is true: # - "disable avatar education message" is enabled # - "sso overrides avatar" is enabled # - "allow uploaded avatars" is disabled - return if SiteSetting.disable_avatar_education_message || SiteSetting.discourse_connect_overrides_avatar || !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + if SiteSetting.disable_avatar_education_message || + SiteSetting.discourse_connect_overrides_avatar || + !TrustLevelAndStaffAndDisabledSetting.matches?(SiteSetting.allow_uploaded_avatars, @user) + return + end # If we got this far, log that we've nagged them about the avatar - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id) + UserHistory.create!( + action: UserHistory.actions[:notified_about_avatar], + target_user_id: @user.id, + ) # Return the message { - id: 'avatar', - templateName: 'education', - body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture")) + id: "avatar", + templateName: "education", + body: + PrettyText.cook( + I18n.t( + "education.avatar", + profile_path: "/u/#{@user.username_lower}/preferences/account#profile-picture", + ), + ), } end @@ -96,39 +119,45 @@ class ComposerMessagesFinder return unless educate_reply?(:notified_about_sequential_replies) # Count the posts made by this user in the last day - recent_posts_user_ids = Post.where(topic_id: @details[:topic_id]) - .where("created_at > ?", 1.day.ago) - .where(post_type: Post.types[:regular]) - .order('created_at desc') - .limit(SiteSetting.sequential_replies_threshold) - .pluck(:user_id) + recent_posts_user_ids = + Post + .where(topic_id: @details[:topic_id]) + .where("created_at > ?", 1.day.ago) + .where(post_type: Post.types[:regular]) + .order("created_at desc") + .limit(SiteSetting.sequential_replies_threshold) + .pluck(:user_id) # Did we get back as many posts as we asked for, and are they all by the current user? - return if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || - recent_posts_user_ids.detect { |u| u != @user.id } + if recent_posts_user_ids.size != SiteSetting.sequential_replies_threshold || + recent_posts_user_ids.detect { |u| u != @user.id } + return + end # If we got this far, log that we've nagged them about the sequential replies - UserHistory.create!(action: UserHistory.actions[:notified_about_sequential_replies], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_sequential_replies], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'sequential_replies', - templateName: 'education', + id: "sequential_replies", + templateName: "education", wait_for_typing: true, - extraClass: 'education-message', + extraClass: "education-message", hide_if_whisper: true, - body: PrettyText.cook(I18n.t('education.sequential_replies')) + body: PrettyText.cook(I18n.t("education.sequential_replies")), } end def check_dominating_topic return unless educate_reply?(:notified_about_dominating_topic) - return if @topic.blank? || - @topic.user_id == @user.id || - @topic.posts_count < SiteSetting.summary_posts_required || - @topic.private_message? + if @topic.blank? || @topic.user_id == @user.id || + @topic.posts_count < SiteSetting.summary_posts_required || @topic.private_message? + return + end posts_by_user = @user.posts.where(topic_id: @topic.id).count @@ -136,16 +165,18 @@ class ComposerMessagesFinder return if ratio < (SiteSetting.dominating_topic_minimum_percent.to_f / 100.0) # Log the topic notification - UserHistory.create!(action: UserHistory.actions[:notified_about_dominating_topic], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_dominating_topic], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) { - id: 'dominating_topic', - templateName: 'dominating-topic', + id: "dominating_topic", + templateName: "dominating-topic", wait_for_typing: true, - extraClass: 'education-message dominating-topic-message', - body: PrettyText.cook(I18n.t('education.dominating_topic')) + extraClass: "education-message dominating-topic-message", + body: PrettyText.cook(I18n.t("education.dominating_topic")), } end @@ -157,73 +188,85 @@ class ComposerMessagesFinder reply_to_user_id = Post.where(id: @details[:post_id]).pluck(:user_id)[0] # Users's last x posts in the topic - last_x_replies = @topic. - posts. - where(user_id: @user.id). - order('created_at desc'). - limit(SiteSetting.get_a_room_threshold). - pluck(:reply_to_user_id). - find_all { |uid| uid != @user.id && uid == reply_to_user_id } + last_x_replies = + @topic + .posts + .where(user_id: @user.id) + .order("created_at desc") + .limit(SiteSetting.get_a_room_threshold) + .pluck(:reply_to_user_id) + .find_all { |uid| uid != @user.id && uid == reply_to_user_id } return unless last_x_replies.size == SiteSetting.get_a_room_threshold - return unless @topic.posts.count('distinct user_id') >= min_users_posted + return unless @topic.posts.count("distinct user_id") >= min_users_posted - UserHistory.create!(action: UserHistory.actions[:notified_about_get_a_room], - target_user_id: @user.id, - topic_id: @details[:topic_id]) + UserHistory.create!( + action: UserHistory.actions[:notified_about_get_a_room], + target_user_id: @user.id, + topic_id: @details[:topic_id], + ) reply_username = User.where(id: last_x_replies[0]).pluck_first(:username) { - id: 'get_a_room', - templateName: 'get-a-room', + id: "get_a_room", + templateName: "get-a-room", wait_for_typing: true, reply_username: reply_username, - extraClass: 'education-message get-a-room', - body: PrettyText.cook( - I18n.t( - 'education.get_a_room', - count: SiteSetting.get_a_room_threshold, - reply_username: reply_username, - base_path: Discourse.base_path - ) - ) + extraClass: "education-message get-a-room", + body: + PrettyText.cook( + I18n.t( + "education.get_a_room", + count: SiteSetting.get_a_room_threshold, + reply_username: reply_username, + base_path: Discourse.base_path, + ), + ), } end def check_reviving_old_topic return unless replying? - return if @topic.nil? || - SiteSetting.warn_reviving_old_topic_age < 1 || - @topic.last_posted_at.nil? || - @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + if @topic.nil? || SiteSetting.warn_reviving_old_topic_age < 1 || @topic.last_posted_at.nil? || + @topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago + return + end { - id: 'reviving_old', - templateName: 'education', + id: "reviving_old", + templateName: "education", wait_for_typing: false, - extraClass: 'education-message', - body: PrettyText.cook( - I18n.t( - 'education.reviving_old_topic', - time_ago: FreedomPatches::Rails4.time_ago_in_words(@topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose') - ) - ) + extraClass: "education-message", + body: + PrettyText.cook( + I18n.t( + "education.reviving_old_topic", + time_ago: + FreedomPatches::Rails4.time_ago_in_words( + @topic.last_posted_at, + false, + scope: :"datetime.distance_in_words_verbose", + ), + ), + ), } end def self.user_not_seen_in_a_while(usernames) - User.where(username_lower: usernames).where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago).pluck(:username).sort + User + .where(username_lower: usernames) + .where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago) + .pluck(:username) + .sort end private def educate_reply?(type) - replying? && - @details[:topic_id] && - (@topic.present? && !@topic.private_message?) && - (@user.post_count >= SiteSetting.educate_until_posts) && - !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) + replying? && @details[:topic_id] && (@topic.present? && !@topic.private_message?) && + (@user.post_count >= SiteSetting.educate_until_posts) && + !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) end def creating_topic? @@ -237,5 +280,4 @@ class ComposerMessagesFinder def editing_post? @details[:composer_action] == "edit" end - end diff --git a/lib/compression/engine.rb b/lib/compression/engine.rb index c098c0b668..732164639d 100644 --- a/lib/compression/engine.rb +++ b/lib/compression/engine.rb @@ -9,12 +9,13 @@ module Compression Compression::Zip.new, Compression::Pipeline.new([Compression::Tar.new, Compression::Gzip.new]), Compression::Gzip.new, - Compression::Tar.new + Compression::Tar.new, ] end def self.engine_for(filename, strategies: default_strategies) - strategy = strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } + strategy = + strategies.detect(-> { raise UnsupportedFileExtension }) { |e| e.can_handle?(filename) } new(strategy) end diff --git a/lib/compression/gzip.rb b/lib/compression/gzip.rb index 4d0b7c51e1..c668b088f7 100644 --- a/lib/compression/gzip.rb +++ b/lib/compression/gzip.rb @@ -3,12 +3,17 @@ module Compression class Gzip < Strategy def extension - '.gz' + ".gz" end def compress(path, target_name) gzip_target = sanitize_path("#{path}/#{target_name}") - Discourse::Utils.execute_command('gzip', '-5', gzip_target, failure_message: "Failed to gzip file.") + Discourse::Utils.execute_command( + "gzip", + "-5", + gzip_target, + failure_message: "Failed to gzip file.", + ) "#{gzip_target}.gz" end @@ -23,7 +28,8 @@ module Compression true end - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) gzip = Zlib::GzipReader.open(compressed_file_path) @@ -32,7 +38,7 @@ module Compression def build_entry_path(dest_path, _, compressed_file_path) basename = File.basename(compressed_file_path) - basename.gsub!(/#{Regexp.escape(extension)}$/, '') + basename.gsub!(/#{Regexp.escape(extension)}$/, "") File.join(dest_path, basename) end @@ -44,12 +50,11 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end # Change this later. - ::File.open(entry_path, 'wb') do |os| - buf = ''.dup + ::File.open(entry_path, "wb") do |os| + buf = "".dup while (buf = entry.read(chunk_size)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/pipeline.rb b/lib/compression/pipeline.rb index 1ea9f17ddb..39a03f9cda 100644 --- a/lib/compression/pipeline.rb +++ b/lib/compression/pipeline.rb @@ -7,25 +7,27 @@ module Compression end def extension - @strategies.reduce('') { |ext, strategy| ext += strategy.extension } + @strategies.reduce("") { |ext, strategy| ext += strategy.extension } end def compress(path, target_name) current_target = target_name - @strategies.reduce('') do |compressed_path, strategy| + @strategies.reduce("") do |compressed_path, strategy| compressed_path = strategy.compress(path, current_target) - current_target = compressed_path.split('/').last + current_target = compressed_path.split("/").last compressed_path end end def decompress(dest_path, compressed_file_path, max_size) - @strategies.reverse.reduce(compressed_file_path) do |to_decompress, strategy| - next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) - FileUtils.rm_rf(to_decompress) - next_compressed_file - end + @strategies + .reverse + .reduce(compressed_file_path) do |to_decompress, strategy| + next_compressed_file = strategy.decompress(dest_path, to_decompress, max_size) + FileUtils.rm_rf(to_decompress) + next_compressed_file + end end end end diff --git a/lib/compression/strategy.rb b/lib/compression/strategy.rb index 8bb54f87d4..d4a05a578c 100644 --- a/lib/compression/strategy.rb +++ b/lib/compression/strategy.rb @@ -18,9 +18,7 @@ module Compression entries_of(compressed_file).each do |entry| entry_path = build_entry_path(sanitized_dest_path, entry, sanitized_compressed_file_path) - if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) - next - end + next if !is_safe_path_for_extraction?(entry_path, sanitized_dest_path) FileUtils.mkdir_p(File.dirname(entry_path)) if is_file?(entry) @@ -45,10 +43,10 @@ module Compression filename.strip.tap do |name| # NOTE: File.basename doesn't work right with Windows paths on Unix # get only the filename, not the whole path - name.sub! /\A.*(\\|\/)/, '' + name.sub! %r{\A.*(\\|/)}, "" # Finally, replace all non alphanumeric, underscore # or periods with underscore - name.gsub! /[^\w\.\-]/, '_' + name.gsub! /[^\w\.\-]/, "_" end end @@ -75,7 +73,7 @@ module Compression raise DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| while (buf = entry.read(chunk_size)) remaining_size -= buf.size raise ExtractFailed if remaining_size.negative? diff --git a/lib/compression/tar.rb b/lib/compression/tar.rb index 580a8ebdd5..3346d1e764 100644 --- a/lib/compression/tar.rb +++ b/lib/compression/tar.rb @@ -1,23 +1,31 @@ # frozen_string_literal: true -require 'rubygems/package' +require "rubygems/package" module Compression class Tar < Strategy def extension - '.tar' + ".tar" end def compress(path, target_name) tar_filename = sanitize_filename("#{target_name}.tar") - Discourse::Utils.execute_command('tar', '--create', '--file', tar_filename, target_name, failure_message: "Failed to tar file.") + Discourse::Utils.execute_command( + "tar", + "--create", + "--file", + tar_filename, + target_name, + failure_message: "Failed to tar file.", + ) sanitize_path("#{path}/#{tar_filename}") end private - def extract_folder(_entry, _entry_path); end + def extract_folder(_entry, _entry_path) + end def get_compressed_file_stream(compressed_file_path) file_stream = IO.new(IO.sysopen(compressed_file_path)) diff --git a/lib/compression/zip.rb b/lib/compression/zip.rb index 63c6b92729..dbf132bf88 100644 --- a/lib/compression/zip.rb +++ b/lib/compression/zip.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'zip' +require "zip" module Compression class Zip < Strategy def extension - '.zip' + ".zip" end def compress(path, target_name) @@ -15,7 +15,7 @@ module Compression ::Zip::File.open(zip_filename, ::Zip::File::CREATE) do |zipfile| if File.directory?(absolute_path) entries = Dir.entries(absolute_path) - %w[. ..] - write_entries(entries, absolute_path, '', zipfile) + write_entries(entries, absolute_path, "", zipfile) else put_into_archive(absolute_path, zipfile, target_name) end @@ -47,15 +47,14 @@ module Compression remaining_size = available_size if ::File.exist?(entry_path) - raise ::Zip::DestinationFileExistsError, - "Destination '#{entry_path}' already exists" + raise ::Zip::DestinationFileExistsError, "Destination '#{entry_path}' already exists" end - ::File.open(entry_path, 'wb') do |os| + ::File.open(entry_path, "wb") do |os| entry.get_input_stream do |is| entry.set_extra_attributes_on_path(entry_path) - buf = ''.dup + buf = "".dup while (buf = is.sysread(chunk_size, buf)) remaining_size -= chunk_size raise ExtractFailed if remaining_size.negative? @@ -70,7 +69,7 @@ module Compression # A helper method to make the recursion work. def write_entries(entries, base_path, path, zipfile) entries.each do |e| - zipfile_path = path == '' ? e : File.join(path, e) + zipfile_path = path == "" ? e : File.join(path, e) disk_file_path = File.join(base_path, zipfile_path) if File.directory? disk_file_path diff --git a/lib/configurable_urls.rb b/lib/configurable_urls.rb index f20321ff6d..c196b74b12 100644 --- a/lib/configurable_urls.rb +++ b/lib/configurable_urls.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module ConfigurableUrls - def faq_path SiteSetting.faq_url.blank? ? "#{Discourse.base_path}/faq" : SiteSetting.faq_url end @@ -11,7 +10,10 @@ module ConfigurableUrls end def privacy_path - SiteSetting.privacy_policy_url.blank? ? "#{Discourse.base_path}/privacy" : SiteSetting.privacy_policy_url + if SiteSetting.privacy_policy_url.blank? + "#{Discourse.base_path}/privacy" + else + SiteSetting.privacy_policy_url + end end - end diff --git a/lib/content_buffer.rb b/lib/content_buffer.rb index 997b57d795..6d6895811f 100644 --- a/lib/content_buffer.rb +++ b/lib/content_buffer.rb @@ -3,7 +3,6 @@ # this class is used to track changes to an arbitrary buffer class ContentBuffer - def initialize(initial_content) @initial_content = initial_content @lines = @initial_content.split("\n") @@ -17,7 +16,6 @@ class ContentBuffer text = transform[:text] if transform[:operation] == :delete - # fix first line l = @lines[start_row] @@ -32,16 +30,13 @@ class ContentBuffer @lines[start_row] = l # remove middle lines - (finish_row - start_row).times do - l = @lines.delete_at start_row + 1 - end + (finish_row - start_row).times { l = @lines.delete_at start_row + 1 } # fix last line @lines[start_row] << @lines[finish_row][finish_col - 1..-1] end if transform[:operation] == :insert - @lines[start_row].insert(start_col, text) split = @lines[start_row].split("\n") @@ -56,7 +51,6 @@ class ContentBuffer @lines.insert(i, "") unless @lines.length > i @lines[i] = split[-1] + @lines[i] end - end end diff --git a/lib/content_security_policy.rb b/lib/content_security_policy.rb index 0cfd309a4b..107dc0437d 100644 --- a/lib/content_security_policy.rb +++ b/lib/content_security_policy.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'content_security_policy/builder' -require 'content_security_policy/extension' +require "content_security_policy/builder" +require "content_security_policy/extension" class ContentSecurityPolicy class << self diff --git a/lib/content_security_policy/builder.rb b/lib/content_security_policy/builder.rb index 4f5dbfb913..e23f55111e 100644 --- a/lib/content_security_policy/builder.rb +++ b/lib/content_security_policy/builder.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy/default' +require "content_security_policy/default" class ContentSecurityPolicy class Builder @@ -33,7 +33,9 @@ class ContentSecurityPolicy def <<(extension) return unless valid_extension?(extension) - extension.each { |directive, sources| extend_directive(normalize_directive(directive), sources) } + extension.each do |directive, sources| + extend_directive(normalize_directive(directive), sources) + end end def build @@ -53,7 +55,7 @@ class ContentSecurityPolicy private def normalize_directive(directive) - directive.to_s.gsub('-', '_').to_sym + directive.to_s.gsub("-", "_").to_sym end def normalize_source(source) diff --git a/lib/content_security_policy/default.rb b/lib/content_security_policy/default.rb index ef67672b05..6b147079c7 100644 --- a/lib/content_security_policy/default.rb +++ b/lib/content_security_policy/default.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Default @@ -7,16 +7,19 @@ class ContentSecurityPolicy def initialize(base_url:) @base_url = base_url - @directives = {}.tap do |directives| - directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https - directives[:base_uri] = [:self] - directives[:object_src] = [:none] - directives[:script_src] = script_src - directives[:worker_src] = worker_src - directives[:report_uri] = report_uri if SiteSetting.content_security_policy_collect_reports - directives[:frame_ancestors] = frame_ancestors if restrict_embed? - directives[:manifest_src] = ["'self'"] - end + @directives = + {}.tap do |directives| + directives[:upgrade_insecure_requests] = [] if SiteSetting.force_https + directives[:base_uri] = [:self] + directives[:object_src] = [:none] + directives[:script_src] = script_src + directives[:worker_src] = worker_src + directives[ + :report_uri + ] = report_uri if SiteSetting.content_security_policy_collect_reports + directives[:frame_ancestors] = frame_ancestors if restrict_embed? + directives[:manifest_src] = ["'self'"] + end end private @@ -27,27 +30,34 @@ class ContentSecurityPolicy SCRIPT_ASSET_DIRECTORIES = [ # [dir, can_use_s3_cdn, can_use_cdn, for_worker] - ['/assets/', true, true, true], - ['/brotli_asset/', true, true, true], - ['/extra-locales/', false, false, false], - ['/highlight-js/', false, true, false], - ['/javascripts/', false, true, true], - ['/plugins/', false, true, true], - ['/theme-javascripts/', false, true, false], - ['/svg-sprite/', false, true, false], + ["/assets/", true, true, true], + ["/brotli_asset/", true, true, true], + ["/extra-locales/", false, false, false], + ["/highlight-js/", false, true, false], + ["/javascripts/", false, true, true], + ["/plugins/", false, true, true], + ["/theme-javascripts/", false, true, false], + ["/svg-sprite/", false, true, false], ] - def script_assets(base = base_url, s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, cdn = GlobalSetting.cdn_url, worker: false) - SCRIPT_ASSET_DIRECTORIES.map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| - next if worker && !for_worker - if can_use_s3_cdn && s3_cdn - s3_cdn + dir - elsif can_use_cdn && cdn - cdn + Discourse.base_path + dir - else - base + dir + def script_assets( + base = base_url, + s3_cdn = GlobalSetting.s3_asset_cdn_url.presence || GlobalSetting.s3_cdn_url, + cdn = GlobalSetting.cdn_url, + worker: false + ) + SCRIPT_ASSET_DIRECTORIES + .map do |dir, can_use_s3_cdn, can_use_cdn, for_worker| + next if worker && !for_worker + if can_use_s3_cdn && s3_cdn + s3_cdn + dir + elsif can_use_cdn && cdn + cdn + Discourse.base_path + dir + else + base + dir + end end - end.compact + .compact end def script_src @@ -55,7 +65,7 @@ class ContentSecurityPolicy "#{base_url}/logs/", "#{base_url}/sidekiq/", "#{base_url}/mini-profiler-resources/", - *script_assets + *script_assets, ].tap do |sources| sources << :report_sample if SiteSetting.content_security_policy_collect_reports sources << :unsafe_eval if Rails.env.development? # TODO remove this once we have proper source maps in dev @@ -67,23 +77,25 @@ class ContentSecurityPolicy end # we need analytics.js still as gtag/js is a script wrapper for it - sources << 'https://www.google-analytics.com/analytics.js' if SiteSetting.ga_universal_tracking_code.present? - sources << 'https://www.googletagmanager.com/gtag/js' if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + if SiteSetting.ga_universal_tracking_code.present? + sources << "https://www.google-analytics.com/analytics.js" + end + if SiteSetting.ga_universal_tracking_code.present? && SiteSetting.ga_version == "v4_gtag" + sources << "https://www.googletagmanager.com/gtag/js" + end if SiteSetting.gtm_container_id.present? - sources << 'https://www.googletagmanager.com/gtm.js' + sources << "https://www.googletagmanager.com/gtm.js" sources << "'nonce-#{ApplicationHelper.google_tag_manager_nonce}'" end - if SiteSetting.splash_screen - sources << "'#{SplashScreenHelper.fingerprint}'" - end + sources << "'#{SplashScreenHelper.fingerprint}'" if SiteSetting.splash_screen end end def worker_src [ "'self'", # For service worker - *script_assets(worker: true) + *script_assets(worker: true), ] end @@ -92,15 +104,11 @@ class ContentSecurityPolicy end def frame_ancestors - [ - "'self'", - *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" } - ] + ["'self'", *EmbeddableHost.pluck(:host).map { |host| "https://#{host}" }] end def restrict_embed? - SiteSetting.content_security_policy_frame_ancestors && - !SiteSetting.embed_any_origin + SiteSetting.content_security_policy_frame_ancestors && !SiteSetting.embed_any_origin end end end diff --git a/lib/content_security_policy/extension.rb b/lib/content_security_policy/extension.rb index 51b59acda3..150e004862 100644 --- a/lib/content_security_policy/extension.rb +++ b/lib/content_security_policy/extension.rb @@ -4,12 +4,12 @@ class ContentSecurityPolicy extend self def site_setting_extension - { script_src: SiteSetting.content_security_policy_script_src.split('|') } + { script_src: SiteSetting.content_security_policy_script_src.split("|") } end def path_specific_extension(path_info) {}.tap do |obj| - for_qunit_route = !Rails.env.production? && ["/qunit", "/wizard/qunit"].include?(path_info) + for_qunit_route = !Rails.env.production? && %w[/qunit /wizard/qunit].include?(path_info) for_qunit_route ||= "/theme-qunit" == path_info obj[:script_src] = :unsafe_eval if for_qunit_route end @@ -23,7 +23,7 @@ class ContentSecurityPolicy end end - THEME_SETTING = 'extend_content_security_policy' + THEME_SETTING = "extend_content_security_policy" def theme_extensions(theme_id) key = "theme_extensions_#{theme_id}" @@ -37,47 +37,55 @@ class ContentSecurityPolicy private def cache - @cache ||= DistributedCache.new('csp_extensions') + @cache ||= DistributedCache.new("csp_extensions") end def find_theme_extensions(theme_id) extensions = [] theme_ids = Theme.transform_ids(theme_id) - Theme.where(id: theme_ids).find_each do |theme| - theme.cached_settings.each do |setting, value| - extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + Theme + .where(id: theme_ids) + .find_each do |theme| + theme.cached_settings.each do |setting, value| + extensions << build_theme_extension(value.split("|")) if setting.to_s == THEME_SETTING + end end - end - extensions << build_theme_extension(ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions) - - html_fields = ThemeField.where( - theme_id: theme_ids, - target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, - name: ThemeField.html_fields + extensions << build_theme_extension( + ThemeModifierHelper.new(theme_ids: theme_ids).csp_extensions, ) + html_fields = + ThemeField.where( + theme_id: theme_ids, + target_id: ThemeField.basic_targets.map { |target| Theme.targets[target.to_sym] }, + name: ThemeField.html_fields, + ) + auto_script_src_extension = { script_src: [] } html_fields.each(&:ensure_baked!) doc = html_fields.map(&:value_baked).join("\n") - Nokogiri::HTML5.fragment(doc).css('script[src]').each do |node| - src = node['src'] - uri = URI(src) + Nokogiri::HTML5 + .fragment(doc) + .css("script[src]") + .each do |node| + src = node["src"] + uri = URI(src) - next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) - next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) - next if uri.path.nil? # Ignore raw hosts + next if GlobalSetting.cdn_url && src.starts_with?(GlobalSetting.cdn_url) # Ignore CDN urls (theme-javascripts) + next if uri.host.nil? # Ignore same-domain scripts (theme-javascripts) + next if uri.path.nil? # Ignore raw hosts - uri.query = nil # CSP should not include query part of url + uri.query = nil # CSP should not include query part of url - uri_string = uri.to_s.sub(/^\/\//, '') # Protocol-less CSP should not have // at beginning of URL + uri_string = uri.to_s.sub(%r{^//}, "") # Protocol-less CSP should not have // at beginning of URL - auto_script_src_extension[:script_src] << uri_string - rescue URI::Error - # Ignore invalid URI - end + auto_script_src_extension[:script_src] << uri_string + rescue URI::Error + # Ignore invalid URI + end extensions << auto_script_src_extension @@ -87,7 +95,7 @@ class ContentSecurityPolicy def build_theme_extension(entries) {}.tap do |extension| entries.each do |entry| - directive, source = entry.split(':', 2).map(&:strip) + directive, source = entry.split(":", 2).map(&:strip) extension[directive] ||= [] extension[directive] << source diff --git a/lib/content_security_policy/middleware.rb b/lib/content_security_policy/middleware.rb index 54ec0f5a65..79ec083427 100644 --- a/lib/content_security_policy/middleware.rb +++ b/lib/content_security_policy/middleware.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require 'content_security_policy' +require "content_security_policy" class ContentSecurityPolicy class Middleware @@ -19,8 +19,16 @@ class ContentSecurityPolicy theme_id = env[:resolved_theme_id] - headers['Content-Security-Policy'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy - headers['Content-Security-Policy-Report-Only'] = policy(theme_id, base_url: base_url, path_info: env["PATH_INFO"]) if SiteSetting.content_security_policy_report_only + headers["Content-Security-Policy"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy + headers["Content-Security-Policy-Report-Only"] = policy( + theme_id, + base_url: base_url, + path_info: env["PATH_INFO"], + ) if SiteSetting.content_security_policy_report_only response end @@ -30,7 +38,7 @@ class ContentSecurityPolicy delegate :policy, to: :ContentSecurityPolicy def html_response?(headers) - headers['Content-Type'] && headers['Content-Type'] =~ /html/ + headers["Content-Type"] && headers["Content-Type"] =~ /html/ end end end diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 4d8df118e6..01463bb776 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -7,7 +7,7 @@ class CookedPostProcessor include CookedProcessorMixin LIGHTBOX_WRAPPER_CSS_CLASS = "lightbox-wrapper" - GIF_SOURCES_REGEXP = /(giphy|tenor)\.com\// + GIF_SOURCES_REGEXP = %r{(giphy|tenor)\.com/} attr_reader :cooking_options, :doc @@ -61,25 +61,27 @@ class CookedPostProcessor return if @post.user.blank? || !Guardian.new.can_see?(@post) BadgeGranter.grant(Badge.find(Badge::FirstEmoji), @post.user, post_id: @post.id) if has_emoji? - BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) if @has_oneboxes - BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) if @post.is_reply_by_email? + if @has_oneboxes + BadgeGranter.grant(Badge.find(Badge::FirstOnebox), @post.user, post_id: @post.id) + end + if @post.is_reply_by_email? + BadgeGranter.grant(Badge.find(Badge::FirstReplyByEmail), @post.user, post_id: @post.id) + end end def post_process_quotes - @doc.css("aside.quote").each do |q| - post_number = q['data-post'] - topic_id = q['data-topic'] - if topic_id && post_number - comparer = QuoteComparer.new( - topic_id.to_i, - post_number.to_i, - q.css('blockquote').text - ) + @doc + .css("aside.quote") + .each do |q| + post_number = q["data-post"] + topic_id = q["data-topic"] + if topic_id && post_number + comparer = QuoteComparer.new(topic_id.to_i, post_number.to_i, q.css("blockquote").text) - q['class'] = ((q['class'] || '') + " quote-post-not-found").strip if comparer.missing? - q['class'] = ((q['class'] || '') + " quote-modified").strip if comparer.modified? + q["class"] = ((q["class"] || "") + " quote-post-not-found").strip if comparer.missing? + q["class"] = ((q["class"] || "") + " quote-modified").strip if comparer.modified? + end end - end end def remove_full_quote_on_direct_reply @@ -87,66 +89,68 @@ class CookedPostProcessor return if @post.post_number == 1 return if @doc.xpath("aside[contains(@class, 'quote')]").size != 1 - previous = Post - .where("post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", @post.post_number, @post.topic_id, Post.types[:regular]) - .order("post_number DESC") - .limit(1) - .pluck(:cooked) - .first + previous = + Post + .where( + "post_number < ? AND topic_id = ? AND post_type = ? AND NOT hidden", + @post.post_number, + @post.topic_id, + Post.types[:regular], + ) + .order("post_number DESC") + .limit(1) + .pluck(:cooked) + .first return if previous.blank? - previous_text = Nokogiri::HTML5::fragment(previous).text.strip + previous_text = Nokogiri::HTML5.fragment(previous).text.strip quoted_text = @doc.css("aside.quote:first-child blockquote").first&.text&.strip || "" return if previous_text.gsub(/(\s){2,}/, '\1') != quoted_text.gsub(/(\s){2,}/, '\1') - quote_regexp = /\A\s*\[quote.+\[\/quote\]/im + quote_regexp = %r{\A\s*\[quote.+\[/quote\]}im quoteless_raw = @post.raw.sub(quote_regexp, "").strip return if @post.raw.strip == quoteless_raw PostRevisor.new(@post).revise!( Discourse.system_user, - { - raw: quoteless_raw, - edit_reason: I18n.t(:removed_direct_reply_full_quotes) - }, + { raw: quoteless_raw, edit_reason: I18n.t(:removed_direct_reply_full_quotes) }, skip_validations: true, - bypass_bump: true + bypass_bump: true, ) end def extract_images # all images with a src attribute @doc.css("img[src], img[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]") - - # minus data images - @doc.css("img[src^='data']") - - # minus emojis - @doc.css("img.emoji") + # minus data images + @doc.css("img[src^='data']") - + # minus emojis + @doc.css("img.emoji") end def extract_images_for_post # all images with a src attribute @doc.css("img[src]") - - # minus emojis - @doc.css("img.emoji") - - # minus images inside quotes - @doc.css(".quote img") - - # minus onebox site icons - @doc.css("img.site-icon") - - # minus onebox avatars - @doc.css("img.onebox-avatar") - - @doc.css("img.onebox-avatar-inline") - - # minus github onebox profile images - @doc.css(".onebox.githubfolder img") + # minus emojis + @doc.css("img.emoji") - + # minus images inside quotes + @doc.css(".quote img") - + # minus onebox site icons + @doc.css("img.site-icon") - + # minus onebox avatars + @doc.css("img.onebox-avatar") - @doc.css("img.onebox-avatar-inline") - + # minus github onebox profile images + @doc.css(".onebox.githubfolder img") end def convert_to_link!(img) w, h = img["width"].to_i, img["height"].to_i - user_width, user_height = (w > 0 && h > 0 && [w, h]) || - get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) + user_width, user_height = + (w > 0 && h > 0 && [w, h]) || get_size_from_attributes(img) || + get_size_from_image_sizes(img["src"], @opts[:image_sizes]) limit_size!(img) @@ -155,7 +159,7 @@ class CookedPostProcessor upload = Upload.get_from_url(src) - original_width, original_height = nil + original_width, original_height = nil if (upload.present?) original_width = upload.width || 0 @@ -172,12 +176,17 @@ class CookedPostProcessor img.add_class("animated") end - return if original_width <= SiteSetting.max_image_width && original_height <= SiteSetting.max_image_height + if original_width <= SiteSetting.max_image_width && + original_height <= SiteSetting.max_image_height + return + end - user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && user_height.to_i <= 0 + user_width, user_height = [original_width, original_height] if user_width.to_i <= 0 && + user_height.to_i <= 0 width, height = user_width, user_height - crop = SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop + crop = + SiteSetting.min_ratio_to_crop > 0 && width.to_f / height.to_f < SiteSetting.min_ratio_to_crop if crop width, height = ImageSizer.crop(width, height) @@ -200,7 +209,7 @@ class CookedPostProcessor return if upload.animated? - if img.ancestors('.onebox, .onebox-body, .quote').blank? && !img.classes.include?("onebox") + if img.ancestors(".onebox, .onebox-body, .quote").blank? && !img.classes.include?("onebox") add_lightbox!(img, original_width, original_height, upload, cropped: crop) end @@ -211,7 +220,7 @@ class CookedPostProcessor def each_responsive_ratio SiteSetting .responsive_post_image_sizes - .split('|') + .split("|") .map(&:to_f) .sort .each { |r| yield r if r > 1 } @@ -239,13 +248,16 @@ class CookedPostProcessor srcset << ", #{cooked_url} #{ratio.to_s.sub(/\.0$/, "")}x" end - img["srcset"] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? + img[ + "srcset" + ] = "#{UrlHelper.cook_url(img["src"], secure: @post.with_secure_uploads?)}#{srcset}" if srcset.present? end else img["src"] = upload.url end - if !@disable_dominant_color && (color = upload.dominant_color(calculate_if_missing: true).presence) + if !@disable_dominant_color && + (color = upload.dominant_color(calculate_if_missing: true).presence) img["data-dominant-color"] = color end end @@ -261,9 +273,7 @@ class CookedPostProcessor a = create_link_node("lightbox", src) img.add_next_sibling(a) - if upload - a["data-download-href"] = Discourse.store.download_url(upload) - end + a["data-download-href"] = Discourse.store.download_url(upload) if upload a.add_child(img) @@ -309,48 +319,55 @@ class CookedPostProcessor @post.update_column(:image_upload_id, upload.id) # post if @post.is_first_post? # topic @post.topic.update_column(:image_upload_id, upload.id) - extra_sizes = ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes + extra_sizes = + ThemeModifierHelper.new(theme_ids: Theme.user_selectable.pluck(:id)).topic_thumbnail_sizes @post.topic.generate_thumbnails!(extra_sizes: extra_sizes) end else @post.update_column(:image_upload_id, nil) if @post.image_upload_id - @post.topic.update_column(:image_upload_id, nil) if @post.topic.image_upload_id && @post.is_first_post? + if @post.topic.image_upload_id && @post.is_first_post? + @post.topic.update_column(:image_upload_id, nil) + end nil end end def optimize_urls - %w{href data-download-href}.each do |selector| - @doc.css("a[#{selector}]").each do |a| - a[selector] = UrlHelper.cook_url(a[selector].to_s) - end + %w[href data-download-href].each do |selector| + @doc.css("a[#{selector}]").each { |a| a[selector] = UrlHelper.cook_url(a[selector].to_s) } end - %w{src}.each do |selector| - @doc.css("img[#{selector}]").each do |img| - custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) - img[selector] = UrlHelper.cook_url( - img[selector].to_s, secure: @post.with_secure_uploads? && !custom_emoji - ) - end + %w[src].each do |selector| + @doc + .css("img[#{selector}]") + .each do |img| + custom_emoji = img["class"]&.include?("emoji-custom") && Emoji.custom?(img["title"]) + img[selector] = UrlHelper.cook_url( + img[selector].to_s, + secure: @post.with_secure_uploads? && !custom_emoji, + ) + end end end def remove_user_ids - @doc.css("a[href]").each do |a| - uri = begin - URI(a["href"]) - rescue URI::Error - next + @doc + .css("a[href]") + .each do |a| + uri = + begin + URI(a["href"]) + rescue URI::Error + next + end + next if uri.hostname != Discourse.current_hostname + + query = Rack::Utils.parse_nested_query(uri.query) + next if !query.delete("u") + + uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence + a["href"] = uri.to_s end - next if uri.hostname != Discourse.current_hostname - - query = Rack::Utils.parse_nested_query(uri.query) - next if !query.delete("u") - - uri.query = query.map { |k, v| "#{k}=#{v}" }.join("&").presence - a["href"] = uri.to_s - end end def enforce_nofollow @@ -369,13 +386,14 @@ class CookedPostProcessor def process_hotlinked_image(img) @hotlinked_map ||= @post.post_hotlinked_media.preload(:upload).map { |r| [r.url, r] }.to_h - normalized_src = PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) + normalized_src = + PostHotlinkedMedia.normalize_src(img["src"] || img[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR]) info = @hotlinked_map[normalized_src] still_an_image = true if info&.too_large? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_large_image_placeholder!(img) else img.remove @@ -383,7 +401,7 @@ class CookedPostProcessor still_an_image = false elsif info&.download_failed? - if img.ancestors('.onebox, .onebox-body').blank? + if img.ancestors(".onebox, .onebox-body").blank? add_broken_image_placeholder!(img) else img.remove @@ -399,28 +417,29 @@ class CookedPostProcessor end def add_blocked_hotlinked_media_placeholders - @doc.css([ - "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", - "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", - ].join(',')).each do |el| - src = el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || - el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(',')&.first&.split(' ')&.first + @doc + .css( + [ + "[#{PrettyText::BLOCKED_HOTLINKED_SRC_ATTR}]", + "[#{PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR}]", + ].join(","), + ) + .each do |el| + src = + el[PrettyText::BLOCKED_HOTLINKED_SRC_ATTR] || + el[PrettyText::BLOCKED_HOTLINKED_SRCSET_ATTR]&.split(",")&.first&.split(" ")&.first - if el.name == "img" - add_blocked_hotlinked_image_placeholder!(el) - next + if el.name == "img" + add_blocked_hotlinked_image_placeholder!(el) + next + end + + el = el.parent if %w[video audio].include?(el.parent.name) + + el = el.parent if el.parent.classes.include?("video-container") + + add_blocked_hotlinked_media_placeholder!(el, src) end - - if ["video", "audio"].include?(el.parent.name) - el = el.parent - end - - if el.parent.classes.include?("video-container") - el = el.parent - end - - add_blocked_hotlinked_media_placeholder!(el, src) - end end def is_svg?(img) @@ -431,6 +450,6 @@ class CookedPostProcessor nil end - File.extname(path) == '.svg' if path + File.extname(path) == ".svg" if path end end diff --git a/lib/cooked_processor_mixin.rb b/lib/cooked_processor_mixin.rb index 68add8893f..4c2fba4c17 100644 --- a/lib/cooked_processor_mixin.rb +++ b/lib/cooked_processor_mixin.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CookedProcessorMixin - def post_process_oneboxes limit = SiteSetting.max_oneboxes_per_post - @doc.css("aside.onebox, a.inline-onebox").size oneboxes = {} @@ -14,7 +13,7 @@ module CookedProcessorMixin if skip_onebox if is_onebox - element.remove_class('onebox') + element.remove_class("onebox") else remove_inline_onebox_loading_class(element) end @@ -26,11 +25,13 @@ module CookedProcessorMixin map[url] = true if is_onebox - onebox = Oneboxer.onebox(url, - invalidate_oneboxes: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + onebox = + Oneboxer.onebox( + url, + invalidate_oneboxes: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) @has_oneboxes = true if onebox.present? onebox @@ -56,7 +57,7 @@ module CookedProcessorMixin # and wrap in a div limit_size!(img) - next if img["class"]&.include?('onebox-avatar') + next if img["class"]&.include?("onebox-avatar") parent = parent&.parent if parent&.name == "a" parent_class = parent && parent["class"] @@ -84,12 +85,18 @@ module CookedProcessorMixin if width < 64 && height < 64 img["class"] = img["class"].to_s + " onebox-full-image" else - img.delete('width') - img.delete('height') - new_parent = img.add_next_sibling("
") + img.delete("width") + img.delete("height") + new_parent = + img.add_next_sibling( + "", + ) new_parent.first.add_child(img) end - elsif (parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || parent_class&.include?("scale-images")) && width > 0 && height > 0 + elsif ( + parent_class&.include?("instagram-images") || parent_class&.include?("tweet-images") || + parent_class&.include?("scale-images") + ) && width > 0 && height > 0 img.remove_attribute("width") img.remove_attribute("height") parent["class"] = "aspect-image-full-size" @@ -98,16 +105,18 @@ module CookedProcessorMixin end if @omit_nofollow || !SiteSetting.add_rel_nofollow_to_user_content - @doc.css(".onebox-body a[rel], .onebox a[rel]").each do |a| - rel_values = a['rel'].split(' ').map(&:downcase) - rel_values.delete('nofollow') - rel_values.delete('ugc') - if rel_values.blank? - a.remove_attribute("rel") - else - a["rel"] = rel_values.join(' ') + @doc + .css(".onebox-body a[rel], .onebox a[rel]") + .each do |a| + rel_values = a["rel"].split(" ").map(&:downcase) + rel_values.delete("nofollow") + rel_values.delete("ugc") + if rel_values.blank? + a.remove_attribute("rel") + else + a["rel"] = rel_values.join(" ") + end end - end end end @@ -116,9 +125,9 @@ module CookedProcessorMixin # 1) the width/height attributes # 2) the dimension from the preview (image_sizes) # 3) the dimension of the original image (HTTP request) - w, h = get_size_from_attributes(img) || - get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || - get_size(img["src"]) + w, h = + get_size_from_attributes(img) || get_size_from_image_sizes(img["src"], @opts[:image_sizes]) || + get_size(img["src"]) # limit the size of the thumbnail img["width"], img["height"] = ImageSizer.resize(w, h) @@ -126,7 +135,7 @@ module CookedProcessorMixin def get_size_from_attributes(img) w, h = img["width"].to_i, img["height"].to_i - return [w, h] unless w <= 0 || h <= 0 + return w, h unless w <= 0 || h <= 0 # if only width or height are specified attempt to scale image if w > 0 || h > 0 w = w.to_f @@ -149,9 +158,9 @@ module CookedProcessorMixin return unless image_sizes.present? image_sizes.each do |image_size| url, size = image_size[0], image_size[1] - if url && src && url.include?(src) && - size && size["width"].to_i > 0 && size["height"].to_i > 0 - return [size["width"], size["height"]] + if url && src && url.include?(src) && size && size["width"].to_i > 0 && + size["height"].to_i > 0 + return size["width"], size["height"] end end nil @@ -165,7 +174,7 @@ module CookedProcessorMixin return @size_cache[url] if @size_cache.has_key?(url) absolute_url = url - absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ /^\/[^\/]/ + absolute_url = Discourse.base_url_no_prefix + absolute_url if absolute_url =~ %r{^/[^/]} return unless absolute_url @@ -186,14 +195,13 @@ module CookedProcessorMixin else @size_cache[url] = FastImage.size(absolute_url) end - rescue Zlib::BufError, URI::Error, OpenSSL::SSL::SSLError # FastImage.size raises BufError for some gifs, leave it. end def is_valid_image_url?(url) uri = URI.parse(url) - %w(http https).include? uri.scheme + %w[http https].include? uri.scheme rescue URI::Error end @@ -217,9 +225,12 @@ module CookedProcessorMixin "help", I18n.t( "upload.placeholders.too_large_humanized", - max_size: ActiveSupport::NumberHelper.number_to_human_size(SiteSetting.max_image_size_kb.kilobytes) - ) - ) + max_size: + ActiveSupport::NumberHelper.number_to_human_size( + SiteSetting.max_image_size_kb.kilobytes, + ), + ), + ), ) # Only if the image is already linked @@ -227,7 +238,7 @@ module CookedProcessorMixin parent = placeholder.parent parent.add_next_sibling(placeholder) - if parent.name == 'a' && parent["href"].present? + if parent.name == "a" && parent["href"].present? if url == parent["href"] parent.remove else @@ -295,12 +306,13 @@ module CookedProcessorMixin end def process_inline_onebox(element) - inline_onebox = InlineOneboxer.lookup( - element.attributes["href"].value, - invalidate: !!@opts[:invalidate_oneboxes], - user_id: @model&.user_id, - category_id: @category_id - ) + inline_onebox = + InlineOneboxer.lookup( + element.attributes["href"].value, + invalidate: !!@opts[:invalidate_oneboxes], + user_id: @model&.user_id, + category_id: @category_id, + ) if title = inline_onebox&.dig(:title) element.children = CGI.escapeHTML(title) diff --git a/lib/crawler_detection.rb b/lib/crawler_detection.rb index 0b90dc0acb..f926d3455d 100644 --- a/lib/crawler_detection.rb +++ b/lib/crawler_detection.rb @@ -4,7 +4,7 @@ module CrawlerDetection WAYBACK_MACHINE_URL = "archive.org" def self.to_matcher(string, type: nil) - escaped = string.split('|').map { |agent| Regexp.escape(agent) }.join('|') + escaped = string.split("|").map { |agent| Regexp.escape(agent) }.join("|") if type == :real && Rails.env == "test" # we need this bypass so we properly render views @@ -15,18 +15,33 @@ module CrawlerDetection end def self.crawler?(user_agent, via_header = nil) - return true if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || via_header&.include?(WAYBACK_MACHINE_URL) + if user_agent.nil? || user_agent&.include?(WAYBACK_MACHINE_URL) || + via_header&.include?(WAYBACK_MACHINE_URL) + return true + end # this is done to avoid regenerating regexes @non_crawler_matchers ||= {} @matchers ||= {} - possibly_real = (@non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher(SiteSetting.non_crawler_user_agents, type: :real)) + possibly_real = + ( + @non_crawler_matchers[SiteSetting.non_crawler_user_agents] ||= to_matcher( + SiteSetting.non_crawler_user_agents, + type: :real, + ) + ) if user_agent.match?(possibly_real) - known_bots = (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) + known_bots = + (@matchers[SiteSetting.crawler_user_agents] ||= to_matcher(SiteSetting.crawler_user_agents)) if user_agent.match?(known_bots) - bypass = (@matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher(SiteSetting.crawler_check_bypass_agents)) + bypass = + ( + @matchers[SiteSetting.crawler_check_bypass_agents] ||= to_matcher( + SiteSetting.crawler_check_bypass_agents, + ) + ) !user_agent.match?(bypass) else false @@ -34,30 +49,40 @@ module CrawlerDetection else true end - end def self.show_browser_update?(user_agent) return false if SiteSetting.browser_update_user_agents.blank? @browser_update_matchers ||= {} - matcher = @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher(SiteSetting.browser_update_user_agents) + matcher = + @browser_update_matchers[SiteSetting.browser_update_user_agents] ||= to_matcher( + SiteSetting.browser_update_user_agents, + ) user_agent.match?(matcher) end # Given a user_agent that returns true from crawler?, should its request be allowed? def self.allow_crawler?(user_agent) - return true if SiteSetting.allowed_crawler_user_agents.blank? && - SiteSetting.blocked_crawler_user_agents.blank? + if SiteSetting.allowed_crawler_user_agents.blank? && + SiteSetting.blocked_crawler_user_agents.blank? + return true + end @allowlisted_matchers ||= {} @blocklisted_matchers ||= {} if SiteSetting.allowed_crawler_user_agents.present? - allowlisted = @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher(SiteSetting.allowed_crawler_user_agents) + allowlisted = + @allowlisted_matchers[SiteSetting.allowed_crawler_user_agents] ||= to_matcher( + SiteSetting.allowed_crawler_user_agents, + ) !user_agent.nil? && user_agent.match?(allowlisted) else - blocklisted = @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher(SiteSetting.blocked_crawler_user_agents) + blocklisted = + @blocklisted_matchers[SiteSetting.blocked_crawler_user_agents] ||= to_matcher( + SiteSetting.blocked_crawler_user_agents, + ) user_agent.nil? || !user_agent.match?(blocklisted) end end diff --git a/lib/csrf_token_verifier.rb b/lib/csrf_token_verifier.rb index 56ed911a7f..8736b749df 100644 --- a/lib/csrf_token_verifier.rb +++ b/lib/csrf_token_verifier.rb @@ -2,7 +2,8 @@ # Provides a way to check a CSRF token outside of a controller class CSRFTokenVerifier - class InvalidCSRFToken < StandardError; end + class InvalidCSRFToken < StandardError + end include ActiveSupport::Configurable include ActionController::RequestForgeryProtection @@ -18,9 +19,7 @@ class CSRFTokenVerifier def call(env) @request = ActionDispatch::Request.new(env.dup) - unless verified_request? - raise InvalidCSRFToken - end + raise InvalidCSRFToken unless verified_request? end public :form_authenticity_token diff --git a/lib/current_user.rb b/lib/current_user.rb index cf84adfb27..fdf7843198 100644 --- a/lib/current_user.rb +++ b/lib/current_user.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module CurrentUser - def self.has_auth_cookie?(env) Discourse.current_user_provider.new(env).has_auth_cookie? end @@ -45,5 +44,4 @@ module CurrentUser def current_user_provider @current_user_provider ||= Discourse.current_user_provider.new(request.env) end - end diff --git a/lib/db_helper.rb b/lib/db_helper.rb index 6bc79b044e..29117934d8 100644 --- a/lib/db_helper.rb +++ b/lib/db_helper.rb @@ -3,7 +3,6 @@ require "migration/base_dropper" class DbHelper - REMAP_SQL ||= <<~SQL SELECT table_name::text, column_name::text, character_maximum_length FROM information_schema.columns @@ -19,24 +18,33 @@ class DbHelper WHERE trigger_name LIKE '%_readonly' SQL - TRUNCATABLE_COLUMNS ||= [ - 'topic_links.url' - ] + TRUNCATABLE_COLUMNS ||= ["topic_links.url"] - def self.remap(from, to, anchor_left: false, anchor_right: false, excluded_tables: [], verbose: false) - like = "#{anchor_left ? '' : "%"}#{from}#{anchor_right ? '' : "%"}" + def self.remap( + from, + to, + anchor_left: false, + anchor_right: false, + excluded_tables: [], + verbose: false + ) + like = "#{anchor_left ? "" : "%"}#{from}#{anchor_right ? "" : "%"}" text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REPLACE(\"#{column[:name]}\", :from, :to)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REPLACE(\"#{column[:name]}\", :from, :to)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" - end.join(" OR ") + where = + columns + .map { |column| "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" LIKE :like" } + .join(" OR ") rows = DB.exec(<<~SQL, from: from, to: to, like: like) UPDATE \"#{table}\" @@ -50,19 +58,32 @@ class DbHelper finish! end - def self.regexp_replace(pattern, replacement, flags: "gi", match: "~*", excluded_tables: [], verbose: false) + def self.regexp_replace( + pattern, + replacement, + flags: "gi", + match: "~*", + excluded_tables: [], + verbose: false + ) text_columns = find_text_columns(excluded_tables) text_columns.each do |table, columns| - set = columns.map do |column| - replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" - replace = truncate(replace, table, column) - "\"#{column[:name]}\" = #{replace}" - end.join(", ") + set = + columns + .map do |column| + replace = "REGEXP_REPLACE(\"#{column[:name]}\", :pattern, :replacement, :flags)" + replace = truncate(replace, table, column) + "\"#{column[:name]}\" = #{replace}" + end + .join(", ") - where = columns.map do |column| - "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" - end.join(" OR ") + where = + columns + .map do |column| + "\"#{column[:name]}\" IS NOT NULL AND \"#{column[:name]}\" #{match} :pattern" + end + .join(" OR ") rows = DB.exec(<<~SQL, pattern: pattern, replacement: replacement, flags: flags, match: match) UPDATE \"#{table}\" @@ -78,23 +99,25 @@ class DbHelper def self.find(needle, anchor_left: false, anchor_right: false, excluded_tables: []) found = {} - like = "#{anchor_left ? '' : "%"}#{needle}#{anchor_right ? '' : "%"}" + like = "#{anchor_left ? "" : "%"}#{needle}#{anchor_right ? "" : "%"}" - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) + DB + .query(REMAP_SQL) + .each do |r| + next if excluded_tables.include?(r.table_name) - rows = DB.query(<<~SQL, like: like) + rows = DB.query(<<~SQL, like: like) SELECT \"#{r.column_name}\" FROM \"#{r.table_name}\" WHERE \""#{r.column_name}"\" LIKE :like SQL - if rows.size > 0 - found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| - row.public_send(r.column_name) + if rows.size > 0 + found["#{r.table_name}.#{r.column_name}"] = rows.map do |row| + row.public_send(r.column_name) + end end end - end found end @@ -112,16 +135,21 @@ class DbHelper triggers = DB.query(TRIGGERS_SQL).map(&:trigger_name).to_set text_columns = Hash.new { |h, k| h[k] = [] } - DB.query(REMAP_SQL).each do |r| - next if excluded_tables.include?(r.table_name) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name)) || - triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + DB + .query(REMAP_SQL) + .each do |r| + if excluded_tables.include?(r.table_name) || + triggers.include?( + Migration::BaseDropper.readonly_trigger_name(r.table_name, r.column_name), + ) || triggers.include?(Migration::BaseDropper.readonly_trigger_name(r.table_name)) + next + end - text_columns[r.table_name] << { - name: r.column_name, - max_length: r.character_maximum_length - } - end + text_columns[r.table_name] << { + name: r.column_name, + max_length: r.character_maximum_length, + } + end text_columns end diff --git a/lib/demon/base.rb b/lib/demon/base.rb index 397fb5d0b0..c49265a4cf 100644 --- a/lib/demon/base.rb +++ b/lib/demon/base.rb @@ -1,26 +1,22 @@ # frozen_string_literal: true -module Demon; end +module Demon +end # intelligent fork based demonizer class Demon::Base - def self.demons @demons end def self.start(count = 1, verbose: false) @demons ||= {} - count.times do |i| - (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start - end + count.times { |i| (@demons["#{prefix}_#{i}"] ||= new(i, verbose: verbose)).start } end def self.stop return unless @demons - @demons.values.each do |demon| - demon.stop - end + @demons.values.each { |demon| demon.stop } end def self.restart @@ -32,16 +28,12 @@ class Demon::Base end def self.ensure_running - @demons.values.each do |demon| - demon.ensure_running - end + @demons.values.each { |demon| demon.ensure_running } end def self.kill(signal) return unless @demons - @demons.values.each do |demon| - demon.kill(signal) - end + @demons.values.each { |demon| demon.kill(signal) } end attr_reader :pid, :parent_pid, :started, :index @@ -83,18 +75,27 @@ class Demon::Base if @pid Process.kill(stop_signal, @pid) - wait_for_stop = lambda { - timeout = @stop_timeout + wait_for_stop = + lambda do + timeout = @stop_timeout - while alive? && timeout > 0 - timeout -= (@stop_timeout / 10.0) - sleep(@stop_timeout / 10.0) - Process.waitpid(@pid, Process::WNOHANG) rescue -1 + while alive? && timeout > 0 + timeout -= (@stop_timeout / 10.0) + sleep(@stop_timeout / 10.0) + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end + end + + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end end - Process.waitpid(@pid, Process::WNOHANG) rescue -1 - } - wait_for_stop.call if alive? @@ -118,7 +119,12 @@ class Demon::Base return end - dead = Process.waitpid(@pid, Process::WNOHANG) rescue -1 + dead = + begin + Process.waitpid(@pid, Process::WNOHANG) + rescue StandardError + -1 + end if dead STDERR.puts "Detected dead worker #{@pid}, restarting..." @pid = nil @@ -141,21 +147,20 @@ class Demon::Base end def run - @pid = fork do - Process.setproctitle("discourse #{self.class.prefix}") - monitor_parent - establish_app - after_fork - end + @pid = + fork do + Process.setproctitle("discourse #{self.class.prefix}") + monitor_parent + establish_app + after_fork + end write_pid_file end def already_running? if File.exist? pid_file pid = File.read(pid_file).to_i - if Demon::Base.alive?(pid) - return pid - end + return pid if Demon::Base.alive?(pid) end nil @@ -164,24 +169,20 @@ class Demon::Base def self.alive?(pid) Process.kill(0, pid) true - rescue + rescue StandardError false end private def verbose(msg) - if @verbose - puts msg - end + puts msg if @verbose end def write_pid_file verbose("writing pid file #{pid_file} for #{@pid}") FileUtils.mkdir_p(@rails_root + "tmp/pids") - File.open(pid_file, 'w') do |f| - f.write(@pid) - end + File.open(pid_file, "w") { |f| f.write(@pid) } end def delete_pid_file diff --git a/lib/demon/email_sync.rb b/lib/demon/email_sync.rb index 12fedc5724..93a627c033 100644 --- a/lib/demon/email_sync.rb +++ b/lib/demon/email_sync.rb @@ -36,15 +36,20 @@ class Demon::EmailSync < ::Demon::Base status = nil idle = false - while @running && group.reload.imap_mailbox_name.present? do + while @running && group.reload.imap_mailbox_name.present? ImapSyncLog.debug("Processing mailbox for group #{group.name} in db #{db}", group) - status = syncer.process( - idle: syncer.can_idle? && status && status[:remaining] == 0, - old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, - ) + status = + syncer.process( + idle: syncer.can_idle? && status && status[:remaining] == 0, + old_emails_limit: status && status[:remaining] > 0 ? 0 : nil, + ) if !syncer.can_idle? && status[:remaining] == 0 - ImapSyncLog.debug("Going to sleep for group #{group.name} in db #{db} to wait for new emails", group, db: false) + ImapSyncLog.debug( + "Going to sleep for group #{group.name} in db #{db} to wait for new emails", + group, + db: false, + ) # Thread goes into sleep for a bit so it is better to return any # connection back to the pool. @@ -66,11 +71,7 @@ class Demon::EmailSync < ::Demon::Base # synchronization primitives available anyway). @running = false - @sync_data.each do |db, sync_data| - sync_data.each do |_, data| - kill_and_disconnect!(data) - end - end + @sync_data.each { |db, sync_data| sync_data.each { |_, data| kill_and_disconnect!(data) } } exit 0 end @@ -89,9 +90,9 @@ class Demon::EmailSync < ::Demon::Base @sync_data = {} @sync_lock = Mutex.new - trap('INT') { kill_threads } - trap('TERM') { kill_threads } - trap('HUP') { kill_threads } + trap("INT") { kill_threads } + trap("TERM") { kill_threads } + trap("HUP") { kill_threads } while @running Discourse.redis.set(HEARTBEAT_KEY, Time.now.to_i, ex: HEARTBEAT_INTERVAL) @@ -101,9 +102,7 @@ class Demon::EmailSync < ::Demon::Base @sync_data.filter! do |db, sync_data| next true if all_dbs.include?(db) - sync_data.each do |_, data| - kill_and_disconnect!(data) - end + sync_data.each { |_, data| kill_and_disconnect!(data) } false end @@ -121,7 +120,10 @@ class Demon::EmailSync < ::Demon::Base next true if groups[group_id] && data[:thread]&.alive? && !data[:syncer]&.disconnected? if !groups[group_id] - ImapSyncLog.warn("Killing thread for group because mailbox is no longer synced", group_id) + ImapSyncLog.warn( + "Killing thread for group because mailbox is no longer synced", + group_id, + ) else ImapSyncLog.warn("Thread for group is dead", group_id) end @@ -133,12 +135,13 @@ class Demon::EmailSync < ::Demon::Base # Spawn new threads for groups that are now synchronized. groups.each do |group_id, group| if !@sync_data[db][group_id] - ImapSyncLog.debug("Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", group, db: false) + ImapSyncLog.debug( + "Starting thread for group #{group.name} mailbox #{group.imap_mailbox_name}", + group, + db: false, + ) - @sync_data[db][group_id] = { - thread: start_thread(db, group), - syncer: nil - } + @sync_data[db][group_id] = { thread: start_thread(db, group), syncer: nil } end end end diff --git a/lib/demon/rails_autospec.rb b/lib/demon/rails_autospec.rb index babf5a5e7e..78f0782046 100644 --- a/lib/demon/rails_autospec.rb +++ b/lib/demon/rails_autospec.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::RailsAutospec < Demon::Base - def self.prefix "rails-autospec" end @@ -17,15 +16,10 @@ class Demon::RailsAutospec < Demon::Base def after_fork require "rack" ENV["RAILS_ENV"] = "test" - Rack::Server.start( - config: "config.ru", - AccessLog: [], - Port: ENV["TEST_SERVER_PORT"] || 60099, - ) + Rack::Server.start(config: "config.ru", AccessLog: [], Port: ENV["TEST_SERVER_PORT"] || 60_099) rescue => e STDERR.puts e.message STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/demon/sidekiq.rb b/lib/demon/sidekiq.rb index ed7556d299..1c1776d80c 100644 --- a/lib/demon/sidekiq.rb +++ b/lib/demon/sidekiq.rb @@ -3,7 +3,6 @@ require "demon/base" class Demon::Sidekiq < ::Demon::Base - def self.prefix "sidekiq" end @@ -26,7 +25,7 @@ class Demon::Sidekiq < ::Demon::Base Demon::Sidekiq.after_fork&.call puts "Loading Sidekiq in process id #{Process.pid}" - require 'sidekiq/cli' + require "sidekiq/cli" cli = Sidekiq::CLI.instance # Unicorn uses USR1 to indicate that log files have been rotated @@ -38,10 +37,10 @@ class Demon::Sidekiq < ::Demon::Base options = ["-c", GlobalSetting.sidekiq_workers.to_s] - [['critical', 8], ['default', 4], ['low', 2], ['ultra_low', 1]].each do |queue_name, weight| + [["critical", 8], ["default", 4], ["low", 2], ["ultra_low", 1]].each do |queue_name, weight| custom_queue_hostname = ENV["UNICORN_SIDEKIQ_#{queue_name.upcase}_QUEUE_HOSTNAME"] - if !custom_queue_hostname || custom_queue_hostname.split(',').include?(Discourse.os_hostname) + if !custom_queue_hostname || custom_queue_hostname.split(",").include?(Discourse.os_hostname) options << "-q" options << "#{queue_name},#{weight}" end @@ -49,7 +48,7 @@ class Demon::Sidekiq < ::Demon::Base # Sidekiq not as high priority as web, in this environment it is forked so a web is very # likely running - Discourse::Utils.execute_command('renice', '-n', '5', '-p', Process.pid.to_s) + Discourse::Utils.execute_command("renice", "-n", "5", "-p", Process.pid.to_s) cli.parse(options) load Rails.root + "config/initializers/100-sidekiq.rb" @@ -59,5 +58,4 @@ class Demon::Sidekiq < ::Demon::Base STDERR.puts e.backtrace.join("\n") exit 1 end - end diff --git a/lib/directory_helper.rb b/lib/directory_helper.rb index a41b117a39..80b2865a85 100644 --- a/lib/directory_helper.rb +++ b/lib/directory_helper.rb @@ -1,24 +1,23 @@ # frozen_string_literal: true module DirectoryHelper - def tmp_directory(prefix) directory_cache[prefix] ||= begin - f = File.join(Rails.root, 'tmp', Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) + f = File.join(Rails.root, "tmp", Time.now.strftime("#{prefix}%Y%m%d%H%M%S")) FileUtils.mkdir_p(f) unless Dir[f].present? f end end def remove_tmp_directory(prefix) - tmp_directory_name = directory_cache[prefix] || '' + tmp_directory_name = directory_cache[prefix] || "" directory_cache.delete(prefix) FileUtils.rm_rf(tmp_directory_name) if Dir[tmp_directory_name].present? end private + def directory_cache @directory_cache ||= {} end - end diff --git a/lib/discourse.rb b/lib/discourse.rb index 68c2e6f210..caba3ba4ce 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -require 'cache' -require 'open3' -require 'plugin/instance' -require 'version' +require "cache" +require "open3" +require "plugin/instance" +require "version" module Discourse DB_POST_MIGRATE_PATH ||= "db/post_migrate" REQUESTED_HOSTNAME ||= "REQUESTED_HOSTNAME" class Utils - URI_REGEXP ||= URI.regexp(%w{http https}) + URI_REGEXP ||= URI.regexp(%w[http https]) # Usage: # Discourse::Utils.execute_command("pwd", chdir: 'mydirectory') @@ -22,7 +22,9 @@ module Discourse runner = CommandRunner.new(**args) if block_given? - raise RuntimeError.new("Cannot pass command and block to execute_command") if command.present? + if command.present? + raise RuntimeError.new("Cannot pass command and block to execute_command") + end yield runner else runner.exec(*command) @@ -33,33 +35,32 @@ module Discourse logs.join("\n") end - def self.logs_markdown(logs, user:, filename: 'log.txt') + def self.logs_markdown(logs, user:, filename: "log.txt") # Reserve 250 characters for the rest of the text max_logs_length = SiteSetting.max_post_length - 250 pretty_logs = Discourse::Utils.pretty_logs(logs) # If logs are short, try to inline them - if pretty_logs.size < max_logs_length - return <<~TEXT + return <<~TEXT if pretty_logs.size < max_logs_length ```text #{pretty_logs} ``` TEXT - end # Try to create an upload for the logs - upload = Dir.mktmpdir do |dir| - File.write(File.join(dir, filename), pretty_logs) - zipfile = Compression::Zip.new.compress(dir, filename) - File.open(zipfile) do |file| - UploadCreator.new( - file, - File.basename(zipfile), - type: 'backup_logs', - for_export: 'true' - ).create_for(user.id) + upload = + Dir.mktmpdir do |dir| + File.write(File.join(dir, filename), pretty_logs) + zipfile = Compression::Zip.new.compress(dir, filename) + File.open(zipfile) do |file| + UploadCreator.new( + file, + File.basename(zipfile), + type: "backup_logs", + for_export: "true", + ).create_for(user.id) + end end - end if upload.persisted? return UploadMarkdown.new(upload).attachment_markdown @@ -82,8 +83,8 @@ module Discourse rescue Errno::ENOENT end - FileUtils.mkdir_p(File.join(Rails.root, 'tmp')) - temp_destination = File.join(Rails.root, 'tmp', SecureRandom.hex) + 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) @@ -101,9 +102,9 @@ module Discourse 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) + FileUtils.mkdir_p(File.join(Rails.root, "tmp")) + temp_destination = File.join(Rails.root, "tmp", SecureRandom.hex) + execute_command("ln", "-s", source, temp_destination) FileUtils.mv(temp_destination, destination) nil @@ -127,13 +128,22 @@ module Discourse end def exec(*command, **exec_params) - raise RuntimeError.new("Cannot specify same parameters at block and command level") if (@init_params.keys & exec_params.keys).present? + if (@init_params.keys & exec_params.keys).present? + raise RuntimeError.new("Cannot specify same parameters at block and command level") + end execute_command(*command, **@init_params.merge(exec_params)) end private - def execute_command(*command, timeout: nil, failure_message: "", success_status_codes: [0], chdir: ".", unsafe_shell: false) + def execute_command( + *command, + timeout: nil, + failure_message: "", + success_status_codes: [0], + chdir: ".", + unsafe_shell: false + ) env = nil env = command.shift if command[0].is_a?(Hash) @@ -156,11 +166,11 @@ module Discourse if !status.exited? || !success_status_codes.include?(status.exitstatus) failure_message = "#{failure_message}\n" if !failure_message.blank? raise CommandError.new( - "#{caller[0]}: #{failure_message}#{stderr}", - stdout: stdout, - stderr: stderr, - status: status - ) + "#{caller[0]}: #{failure_message}#{stderr}", + stdout: stdout, + stderr: stderr, + status: status, + ) end stdout @@ -195,33 +205,32 @@ module Discourse # mini_scheduler direct reporting if Hash === job job_class = job["class"] - if job_class - job_exception_stats[job_class] += 1 - end + job_exception_stats[job_class] += 1 if job_class end # internal reporting - if job.class == Class && ::Jobs::Base > job - job_exception_stats[job] += 1 - end + 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)) + parent_logger.handle_exception( + ex, + { current_db: cm.current_db, current_hostname: cm.current_hostname }.merge(context), + ) raise ex if Rails.env.test? end # Expected less matches than what we got in a find - class TooManyMatches < StandardError; end + class TooManyMatches < StandardError + end # When they try to do something they should be logged in for - class NotLoggedIn < StandardError; end + class NotLoggedIn < StandardError + end # When the input is somehow bad - class InvalidParameters < StandardError; end + class InvalidParameters < StandardError + end # When they don't have permission to do something class InvalidAccess < StandardError @@ -249,7 +258,13 @@ module Discourse attr_reader :original_path attr_reader :custom_message - def initialize(msg = nil, status: 404, check_permalinks: false, original_path: nil, custom_message: nil) + def initialize( + msg = nil, + status: 404, + check_permalinks: false, + original_path: nil, + custom_message: nil + ) super(msg) @status = status @@ -260,27 +275,33 @@ module Discourse end # When a setting is missing - class SiteSettingMissing < StandardError; end + class SiteSettingMissing < StandardError + end # When ImageMagick is missing - class ImageMagickMissing < StandardError; end + class ImageMagickMissing < StandardError + end # When read-only mode is enabled - class ReadOnly < StandardError; end + class ReadOnly < StandardError + end # Cross site request forgery - class CSRF < StandardError; end + class CSRF < StandardError + end - class Deprecation < StandardError; end + class Deprecation < StandardError + end - class ScssError < StandardError; end + class ScssError < StandardError + end def self.filters - @filters ||= [:latest, :unread, :new, :unseen, :top, :read, :posted, :bookmarks] + @filters ||= %i[latest unread new unseen top read posted bookmarks] end def self.anonymous_filters - @anonymous_filters ||= [:latest, :top, :categories] + @anonymous_filters ||= %i[latest top categories] end def self.top_menu_items @@ -288,7 +309,7 @@ module Discourse end def self.anonymous_top_menu_items - @anonymous_top_menu_items ||= Discourse.anonymous_filters + [:categories, :top] + @anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top] end PIXEL_RATIOS ||= [1, 1.5, 2, 3] @@ -297,26 +318,28 @@ module Discourse # 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).to_i - end - end + SiteSetting + .avatar_sizes + .split("|") + .map(&:to_i) + .each { |size| PIXEL_RATIOS.each { |pixel_ratio| set << (size * pixel_ratio).to_i } } set end def self.activate_plugins! @plugins = [] - Plugin::Instance.find_all("#{Rails.root}/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})" + Plugin::Instance + .find_all("#{Rails.root}/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 - end DiscourseEvent.trigger(:after_plugin_activation) end @@ -360,9 +383,7 @@ module Discourse def self.apply_asset_filters(plugins, type, request) filter_opts = asset_filter_options(type, request) - plugins.select do |plugin| - plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } - end + plugins.select { |plugin| plugin.asset_filters.all? { |b| b.call(type, request, filter_opts) } } end def self.asset_filter_options(type, request) @@ -385,20 +406,24 @@ module Discourse targets << :desktop if args[:desktop_view] targets.each do |target| - assets += plugins.find_all do |plugin| - plugin.css_asset_exists?(target) - end.map do |plugin| - target.nil? ? plugin.directory_name : "#{plugin.directory_name}_#{target}" - end + 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 end 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 = + 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]) @@ -413,25 +438,33 @@ module Discourse end def self.assets_digest - @assets_digest ||= begin - digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) + @assets_digest ||= + begin + digest = Digest::MD5.hexdigest(ActionView::Base.assets_manifest.assets.values.sort.join) - channel = "/global/asset-version" - message = MessageBus.last_message(channel) + channel = "/global/asset-version" + message = MessageBus.last_message(channel) - unless message && message.data == digest - MessageBus.publish channel, digest + MessageBus.publish channel, digest unless message && message.data == digest + digest end - digest - end end BUILTIN_AUTH ||= [ - 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::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"), - Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord") + Auth::AuthProvider.new(authenticator: Auth::DiscordAuthenticator.new, icon: "fab-discord"), ] def self.auth_providers @@ -439,7 +472,7 @@ module Discourse end def self.enabled_auth_providers - auth_providers.select { |provider| provider.authenticator.enabled? } + auth_providers.select { |provider| provider.authenticator.enabled? } end def self.authenticators @@ -449,17 +482,18 @@ module Discourse end def self.enabled_authenticators - authenticators.select { |authenticator| authenticator.enabled? } + authenticators.select { |authenticator| authenticator.enabled? } end def self.cache - @cache ||= begin - if GlobalSetting.skip_redis? - ActiveSupport::Cache::MemoryStore.new - else - Cache.new + @cache ||= + begin + if GlobalSetting.skip_redis? + ActiveSupport::Cache::MemoryStore.new + else + Cache.new + end end - end end # hostname of the server, operating system level @@ -467,15 +501,15 @@ module Discourse def self.os_hostname @os_hostname ||= begin - require 'socket' + require "socket" Socket.gethostname rescue => e - warn_exception(e, message: 'Socket.gethostname is not working') + 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' + warn_exception(e, message: "hostname command is not working") + "unknown_host" end end end @@ -501,12 +535,12 @@ module Discourse def self.current_hostname_with_port default_port = SiteSetting.force_https? ? 443 : 80 result = +"#{current_hostname}" - result << ":#{SiteSetting.port}" if SiteSetting.port.to_i > 0 && SiteSetting.port.to_i != default_port - - if Rails.env.development? && SiteSetting.port.blank? - result << ":#{ENV["UNICORN_PORT"] || 3000}" + 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 @@ -520,16 +554,18 @@ module Discourse def self.route_for(uri) unless uri.is_a?(URI) - uri = begin - URI(uri) - rescue ArgumentError, URI::Error - end + 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)) + 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 @@ -543,21 +579,21 @@ module Discourse alias_method :base_url_no_path, :base_url_no_prefix end - READONLY_MODE_KEY_TTL ||= 60 - READONLY_MODE_KEY ||= 'readonly_mode' - PG_READONLY_MODE_KEY ||= 'readonly_mode:postgres' - PG_READONLY_MODE_KEY_TTL ||= 300 - USER_READONLY_MODE_KEY ||= 'readonly_mode:user' - PG_FORCE_READONLY_MODE_KEY ||= 'readonly_mode:postgres_force' + READONLY_MODE_KEY_TTL ||= 60 + READONLY_MODE_KEY ||= "readonly_mode" + PG_READONLY_MODE_KEY ||= "readonly_mode:postgres" + PG_READONLY_MODE_KEY_TTL ||= 300 + USER_READONLY_MODE_KEY ||= "readonly_mode:user" + PG_FORCE_READONLY_MODE_KEY ||= "readonly_mode:postgres_force" # Psuedo readonly mode, where staff can still write - STAFF_WRITES_ONLY_MODE_KEY ||= 'readonly_mode:staff_writes_only' + STAFF_WRITES_ONLY_MODE_KEY ||= "readonly_mode:staff_writes_only" READONLY_KEYS ||= [ READONLY_MODE_KEY, PG_READONLY_MODE_KEY, USER_READONLY_MODE_KEY, - PG_FORCE_READONLY_MODE_KEY + PG_FORCE_READONLY_MODE_KEY, ] def self.enable_readonly_mode(key = READONLY_MODE_KEY) @@ -565,7 +601,9 @@ module Discourse 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) + if [USER_READONLY_MODE_KEY, PG_FORCE_READONLY_MODE_KEY, STAFF_WRITES_ONLY_MODE_KEY].include?( + key, + ) Discourse.redis.set(key, 1) else ttl = @@ -595,15 +633,13 @@ module Discourse unless @threads[key]&.alive? @threads[key] = Thread.new do - while @dbs.size > 0 do + while @dbs.size > 0 sleep ttl / 2 @mutex.synchronize do @dbs.each do |db| RailsMultisite::ConnectionManagement.with_connection(db) do - if !Discourse.redis.expire(key, ttl) - @dbs.delete(db) - end + @dbs.delete(db) if !Discourse.redis.expire(key, ttl) end end end @@ -653,7 +689,7 @@ module Discourse # Shared between processes def self.postgres_last_read_only - @postgres_last_read_only ||= DistributedCache.new('postgres_last_read_only', namespace: false) + @postgres_last_read_only ||= DistributedCache.new("postgres_last_read_only", namespace: false) end # Per-process @@ -698,39 +734,43 @@ module Discourse # 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) + MessageBus.publish("/refresh_client", "clobber", user_ids: user_ids) else - MessageBus.publish('/global/asset-version', 'clobber') + MessageBus.publish("/global/asset-version", "clobber") end end def self.git_version - @git_version ||= begin - git_cmd = 'git rev-parse HEAD' - self.try_git(git_cmd, Discourse::VERSION::STRING) - end + @git_version ||= + begin + git_cmd = "git rev-parse HEAD" + self.try_git(git_cmd, Discourse::VERSION::STRING) + end end def self.git_branch - @git_branch ||= begin - git_cmd = 'git rev-parse --abbrev-ref HEAD' - self.try_git(git_cmd, 'unknown') - end + @git_branch ||= + begin + git_cmd = "git rev-parse --abbrev-ref HEAD" + self.try_git(git_cmd, "unknown") + end end 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 + @full_version ||= + begin + git_cmd = 'git describe --dirty --match "v[0-9]*" 2> /dev/null' + self.try_git(git_cmd, "unknown") + end end 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 + @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 def self.try_git(git_cmd, default_value) @@ -738,20 +778,21 @@ module Discourse begin version_value = `#{git_cmd}`.strip - rescue + rescue StandardError version_value = default_value end - if version_value.empty? - version_value = default_value - end + version_value = default_value if version_value.empty? version_value end # Either returns the site_contact_username user or the first admin. def self.site_contact_user - user = User.find_by(username_lower: SiteSetting.site_contact_username.downcase) if SiteSetting.site_contact_username.present? + 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 @@ -765,10 +806,10 @@ module Discourse def self.store if SiteSetting.Upload.enable_s3_uploads - @s3_store_loaded ||= require 'file_store/s3_store' + @s3_store_loaded ||= require "file_store/s3_store" FileStore::S3Store.new else - @local_store_loaded ||= require 'file_store/local_store' + @local_store_loaded ||= require "file_store/local_store" FileStore::LocalStore.new end end @@ -805,15 +846,15 @@ module Discourse Discourse.cache.reconnect Logster.store.redis.reconnect # shuts down all connections in the pool - Sidekiq.redis_pool.shutdown { |conn| conn.disconnect! } + 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 + 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 @@ -831,7 +872,7 @@ module Discourse # 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(" ") : "" + append = env ? (+" ") << env.map { |k, v| "#{k}: #{v}" }.join(" ") : "" if !(Logster::Logger === Rails.logger) Rails.logger.warn("#{message}#{append}") @@ -839,9 +880,7 @@ module Discourse end loggers = [Rails.logger] - if Rails.logger.chained - loggers.concat(Rails.logger.chained) - end + loggers.concat(Rails.logger.chained) if Rails.logger.chained logster_env = env @@ -849,9 +888,7 @@ module Discourse logster_env = Logster::Message.populate_from_env(old_env) # a bit awkward by try to keep the new params - env.each do |k, v| - logster_env[k] = v - end + env.each { |k, v| logster_env[k] = v } end loggers.each do |logger| @@ -860,12 +897,7 @@ module Discourse next end - logger.store.report( - ::Logger::Severity::WARN, - "discourse", - message, - env: logster_env - ) + logger.store.report(::Logger::Severity::WARN, "discourse", message, env: logster_env) end if old_env @@ -881,7 +913,6 @@ module Discourse # report a warning maintaining backtrack for logster def self.warn_exception(e, message: "", env: nil) if Rails.logger.respond_to? :add_with_opts - env ||= {} env[:current_db] ||= RailsMultisite::ConnectionManagement.current_db @@ -891,13 +922,13 @@ module Discourse "#{message} : #{e.class.name} : #{e}", "discourse-exception", backtrace: e.backtrace.join("\n"), - env: env + env: env, ) else # no logster ... fallback Rails.logger.warn("#{message} #{e}\n#{e.backtrace.join("\n")}") end - rescue + rescue StandardError STDERR.puts "Failed to report exception #{e} #{message}" end @@ -909,17 +940,11 @@ module Discourse warning << "\nAt #{location}" warning = warning.join(" ") - if raise_error - raise Deprecation.new(warning) - end + raise Deprecation.new(warning) if raise_error - if Rails.env == "development" - STDERR.puts(warning) - end + STDERR.puts(warning) if Rails.env == "development" - if output_in_test && Rails.env == "test" - STDERR.puts(warning) - end + STDERR.puts(warning) if output_in_test && Rails.env == "test" digest = Digest::MD5.hexdigest(warning) redis_key = "deprecate-notice-#{digest}" @@ -935,7 +960,7 @@ module Discourse warning end - SIDEKIQ_NAMESPACE ||= 'sidekiq' + SIDEKIQ_NAMESPACE ||= "sidekiq" def self.sidekiq_redis_config conf = GlobalSetting.redis_config.dup @@ -951,7 +976,8 @@ module Discourse 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) + 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 @@ -961,7 +987,11 @@ module Discourse def self.reset_active_record_cache ActiveRecord::Base.connection.query_cache.clear (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.reset_column_information rescue nil + begin + table.classify.constantize.reset_column_information + rescue StandardError + nil + end end nil end @@ -971,7 +1001,7 @@ module Discourse end def self.skip_post_deployment_migrations? - ['1', 'true'].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) + %w[1 true].include?(ENV["SKIP_POST_DEPLOYMENT_MIGRATIONS"]&.to_s) end # this is used to preload as much stuff as possible prior to forking @@ -985,7 +1015,11 @@ module Discourse # load up all models and schema (ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| - table.classify.constantize.first rescue nil + begin + table.classify.constantize.first + rescue StandardError + nil + end end # ensure we have a full schema cache in case we missed something above @@ -1024,29 +1058,27 @@ module Discourse end [ - Thread.new { + Thread.new do # router warm up - Rails.application.routes.recognize_path('abc') rescue nil - }, - Thread.new { + 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 - }, - Thread.new { - require 'actionview_precompiler' + end, + Thread.new do + require "actionview_precompiler" ActionviewPrecompiler.precompile - }, - Thread.new { - LetterAvatar.image_magick_version - }, - Thread.new { - SvgSprite.core_svgs - }, - Thread.new { - EmberCli.script_chunks - } + end, + Thread.new { LetterAvatar.image_magick_version }, + Thread.new { SvgSprite.core_svgs }, + Thread.new { EmberCli.script_chunks }, ].each(&:join) ensure @preloaded_rails = true @@ -1055,10 +1087,10 @@ module Discourse mattr_accessor :redis def self.is_parallel_test? - ENV['RAILS_ENV'] == "test" && ENV['TEST_ENV_NUMBER'] + ENV["RAILS_ENV"] == "test" && ENV["TEST_ENV_NUMBER"] end - CDN_REQUEST_METHODS ||= ["GET", "HEAD", "OPTIONS"] + CDN_REQUEST_METHODS ||= %w[GET HEAD OPTIONS] def self.is_cdn_request?(env, request_method) return unless CDN_REQUEST_METHODS.include?(request_method) @@ -1071,8 +1103,8 @@ module Discourse end def self.apply_cdn_headers(headers) - headers['Access-Control-Allow-Origin'] = '*' - headers['Access-Control-Allow-Methods'] = CDN_REQUEST_METHODS.join(", ") + headers["Access-Control-Allow-Origin"] = "*" + headers["Access-Control-Allow-Methods"] = CDN_REQUEST_METHODS.join(", ") headers end @@ -1091,8 +1123,12 @@ module Discourse end 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 = + 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 end diff --git a/lib/discourse_connect_base.rb b/lib/discourse_connect_base.rb index 3a503ddcbe..b5e04d8f21 100644 --- a/lib/discourse_connect_base.rb +++ b/lib/discourse_connect_base.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true class DiscourseConnectBase + class ParseError < RuntimeError + end - class ParseError < RuntimeError; end - - ACCESSORS = %i{ + ACCESSORS = %i[ add_groups - admin moderator + admin + moderator avatar_force_update avatar_url bio @@ -31,11 +32,11 @@ class DiscourseConnectBase title username website - } + ] FIXNUMS = [] - BOOLS = %i{ + BOOLS = %i[ admin avatar_force_update confirmed_2fa @@ -46,7 +47,7 @@ class DiscourseConnectBase require_2fa require_activation suppress_welcome_message - } + ] def self.nonce_expiry_time @nonce_expiry_time ||= 10.minutes @@ -80,9 +81,11 @@ class DiscourseConnectBase decoded_hash = Rack::Utils.parse_query(decoded) if sso.sign(parsed["sso"]) != parsed["sig"] - diags = "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" - if parsed["sso"] =~ /[^a-zA-Z0-9=\r\n\/+]/m - raise ParseError, "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" + diags = + "\n\nsso: #{parsed["sso"]}\n\nsig: #{parsed["sig"]}\n\nexpected sig: #{sso.sign(parsed["sso"])}" + if parsed["sso"] =~ %r{[^a-zA-Z0-9=\r\n/+]}m + raise ParseError, + "The SSO field should be Base64 encoded, using only A-Z, a-z, 0-9, +, /, and = characters. Your input contains characters we don't understand as Base64, see http://en.wikipedia.org/wiki/Base64 #{diags}" else raise ParseError, "Bad signature for payload #{diags}" end @@ -91,9 +94,7 @@ class DiscourseConnectBase ACCESSORS.each do |k| val = decoded_hash[k.to_s] val = val.to_i if FIXNUMS.include? k - if BOOLS.include? k - val = ["true", "false"].include?(val) ? val == "true" : nil - end + val = %w[true false].include?(val) ? val == "true" : nil if BOOLS.include? k sso.public_send("#{k}=", val) end @@ -137,12 +138,12 @@ class DiscourseConnectBase def to_url(base_url = nil) base = "#{base_url || sso_url}" - "#{base}#{base.include?('?') ? '&' : '?'}#{payload}" + "#{base}#{base.include?("?") ? "&" : "?"}#{payload}" end def payload(secret = nil) payload = Base64.strict_encode64(unsigned_payload) - "sso=#{CGI::escape(payload)}&sig=#{sign(payload, secret)}" + "sso=#{CGI.escape(payload)}&sig=#{sign(payload, secret)}" end def unsigned_payload @@ -157,9 +158,7 @@ class DiscourseConnectBase payload[k] = val end - @custom_fields&.each do |k, v| - payload["custom.#{k}"] = v.to_s - end + @custom_fields&.each { |k, v| payload["custom.#{k}"] = v.to_s } payload end diff --git a/lib/discourse_connect_provider.rb b/lib/discourse_connect_provider.rb index 218d081b10..76f46989df 100644 --- a/lib/discourse_connect_provider.rb +++ b/lib/discourse_connect_provider.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true class DiscourseConnectProvider < DiscourseConnectBase - class BlankSecret < RuntimeError; end - class BlankReturnUrl < RuntimeError; end + class BlankSecret < RuntimeError + end + class BlankReturnUrl < RuntimeError + end def self.parse(payload, sso_secret = nil, **init_kwargs) parsed_payload = Rack::Utils.parse_query(payload) @@ -15,11 +17,16 @@ class DiscourseConnectProvider < DiscourseConnectBase if sso_secret.blank? begin host = URI.parse(return_sso_url).host - Rails.logger.warn("SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings") + Rails.logger.warn( + "SSO failed; website #{host} is not in the `discourse_connect_provider_secrets` site settings", + ) rescue StandardError => e # going for StandardError cause URI::Error may not be enough, eg it parses to something not # responding to host - Discourse.warn_exception(e, message: "SSO failed; invalid or missing return_sso_url in SSO payload") + Discourse.warn_exception( + e, + message: "SSO failed; invalid or missing return_sso_url in SSO payload", + ) end raise BlankSecret @@ -31,7 +38,7 @@ class DiscourseConnectProvider < DiscourseConnectBase def self.lookup_return_sso_url(parsed_payload) decoded = Base64.decode64(parsed_payload["sso"]) decoded_hash = Rack::Utils.parse_query(decoded) - decoded_hash['return_sso_url'] + decoded_hash["return_sso_url"] end def self.lookup_sso_secret(return_sso_url, parsed_payload) @@ -39,21 +46,23 @@ class DiscourseConnectProvider < DiscourseConnectBase return_url_host = URI.parse(return_sso_url).host - provider_secrets = SiteSetting - .discourse_connect_provider_secrets - .split("\n") - .map { |row| row.split("|", 2) } - .sort_by { |k, _| k } - .reverse + provider_secrets = + SiteSetting + .discourse_connect_provider_secrets + .split("\n") + .map { |row| row.split("|", 2) } + .sort_by { |k, _| k } + .reverse first_domain_match = nil - pair = provider_secrets.find do |domain, configured_secret| - if WildcardDomainChecker.check_domain(domain, return_url_host) - first_domain_match ||= configured_secret - sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + pair = + provider_secrets.find do |domain, configured_secret| + if WildcardDomainChecker.check_domain(domain, return_url_host) + first_domain_match ||= configured_secret + sign(parsed_payload["sso"], configured_secret) == parsed_payload["sig"] + end end - end # falls back to a secret which will fail to validate in DiscourseConnectBase # this ensures error flow is correct diff --git a/lib/discourse_dev/category.rb b/lib/discourse_dev/category.rb index b13c43d758..7c1fe2500c 100644 --- a/lib/discourse_dev/category.rb +++ b/lib/discourse_dev/category.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Category < Record - def initialize super(::Category, DiscourseDev.config.category[:count]) @parent_category_ids = ::Category.where(parent_category_id: nil).pluck(:id) @@ -29,7 +28,7 @@ module DiscourseDev description: Faker::Lorem.paragraph, user_id: ::Discourse::SYSTEM_USER_ID, color: Faker::Color.hex_color.last(6), - parent_category_id: parent_category_id + parent_category_id: parent_category_id, } end diff --git a/lib/discourse_dev/config.rb b/lib/discourse_dev/config.rb index eb4a1d160b..f83eebe880 100644 --- a/lib/discourse_dev/config.rb +++ b/lib/discourse_dev/config.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rails' -require 'highline/import' +require "rails" +require "highline/import" module DiscourseDev class Config @@ -63,10 +63,11 @@ module DiscourseDev if settings.present? email = settings[:email] || "new_user@example.com" - new_user = ::User.create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email) - ) + new_user = + ::User.create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + ) new_user.email_tokens.update_all confirmed: true new_user.activate end @@ -88,15 +89,14 @@ module DiscourseDev def create_admin_user_from_settings(settings) email = settings[:email] - admin = ::User.with_email(email).first_or_create!( - email: email, - username: settings[:username] || UserNameSuggester.suggest(email), - password: settings[:password] - ) + admin = + ::User.with_email(email).first_or_create!( + email: email, + username: settings[:username] || UserNameSuggester.suggest(email), + password: settings[:password], + ) admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate end @@ -107,10 +107,7 @@ module DiscourseDev password = ask("Password (optional, press ENTER to skip): ") username = UserNameSuggester.suggest(email) - admin = ::User.new( - email: email, - username: username - ) + admin = ::User.new(email: email, username: username) if password.present? admin.password = password @@ -122,7 +119,7 @@ module DiscourseDev saved = admin.save if saved - File.open(file_path, 'a') do | file| + File.open(file_path, "a") do |file| file.puts("admin:") file.puts(" username: #{admin.username}") file.puts(" email: #{admin.email}") @@ -137,9 +134,7 @@ module DiscourseDev admin.save admin.grant_admin! - if admin.trust_level < 1 - admin.change_trust_level!(1) - end + admin.change_trust_level!(1) if admin.trust_level < 1 admin.email_tokens.update_all confirmed: true admin.activate diff --git a/lib/discourse_dev/group.rb b/lib/discourse_dev/group.rb index c346b0c5fa..91a0ffcb7e 100644 --- a/lib/discourse_dev/group.rb +++ b/lib/discourse_dev/group.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Group < Record - def initialize super(::Group, DiscourseDev.config.group[:count]) end diff --git a/lib/discourse_dev/post.rb b/lib/discourse_dev/post.rb index ef0e072daa..350696a7d8 100644 --- a/lib/discourse_dev/post.rb +++ b/lib/discourse_dev/post.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Post < Record - attr_reader :topic def initialize(topic, count) @@ -28,7 +27,7 @@ module DiscourseDev raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), created_at: Faker::Time.between(from: topic.last_posted_at, to: DateTime.now), skip_validations: true, - skip_guardian: true + skip_guardian: true, } end @@ -44,13 +43,20 @@ module DiscourseDev def generate_likes(post) user_ids = [post.user_id] - Faker::Number.between(from: 0, to: @max_likes_count).times do - user = self.user - next if user_ids.include?(user.id) + Faker::Number + .between(from: 0, to: @max_likes_count) + .times do + user = self.user + next if user_ids.include?(user.id) - PostActionCreator.new(user, post, PostActionType.types[:like], created_at: Faker::Time.between(from: post.created_at, to: DateTime.now)).perform - user_ids << user.id - end + PostActionCreator.new( + user, + post, + PostActionType.types[:like], + created_at: Faker::Time.between(from: post.created_at, to: DateTime.now), + ).perform + user_ids << user.id + end end def user @@ -90,13 +96,14 @@ module DiscourseDev count.times do |i| begin user = User.random - reply = Faker::DiscourseMarkdown.with_user(user.id) do - { - topic_id: topic.id, - raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), - skip_validations: true - } - end + reply = + Faker::DiscourseMarkdown.with_user(user.id) do + { + topic_id: topic.id, + raw: Faker::DiscourseMarkdown.sandwich(sentences: 5), + skip_validations: true, + } + end PostCreator.new(user, reply).create! rescue ActiveRecord::RecordNotSaved => e puts e @@ -109,6 +116,5 @@ module DiscourseDev def self.random super(::Post) end - end end diff --git a/lib/discourse_dev/post_revision.rb b/lib/discourse_dev/post_revision.rb index 7c1f1da644..0c282e9540 100644 --- a/lib/discourse_dev/post_revision.rb +++ b/lib/discourse_dev/post_revision.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class PostRevision < Record - def initialize super(::PostRevision, DiscourseDev.config.post_revisions[:count]) end diff --git a/lib/discourse_dev/record.rb b/lib/discourse_dev/record.rb index 3e2767ef62..b1b84097df 100644 --- a/lib/discourse_dev/record.rb +++ b/lib/discourse_dev/record.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'discourse_dev' -require 'rails' -require 'faker' +require "discourse_dev" +require "rails" +require "faker" module DiscourseDev class Record @@ -12,11 +12,12 @@ module DiscourseDev attr_reader :model, :type def initialize(model, count = DEFAULT_COUNT) - @@initialized ||= begin - Faker::Discourse.unique.clear - RateLimiter.disable - true - end + @@initialized ||= + begin + Faker::Discourse.unique.clear + RateLimiter.disable + true + end @model = model @type = model.to_s.downcase.to_sym @@ -40,11 +41,9 @@ module DiscourseDev if current_count >= @count puts "Already have #{current_count} #{type} records" - Rake.application.top_level_tasks.each do |task_name| - Rake::Task[task_name].reenable - end + Rake.application.top_level_tasks.each { |task_name| Rake::Task[task_name].reenable } - Rake::Task['dev:repopulate'].invoke + Rake::Task["dev:repopulate"].invoke return elsif current_count > 0 @count -= current_count @@ -74,7 +73,9 @@ module DiscourseDev end def self.random(model, use_existing_records: true) - model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") if !use_existing_records && model.new.respond_to?(:custom_fields) + if !use_existing_records && model.new.respond_to?(:custom_fields) + model.joins(:_custom_fields).where("#{:type}_custom_fields.name = '#{AUTO_POPULATED}'") + end count = model.count raise "#{:type} records are not yet populated" if count == 0 diff --git a/lib/discourse_dev/tag.rb b/lib/discourse_dev/tag.rb index 987d8656e9..96c06ad6e1 100644 --- a/lib/discourse_dev/tag.rb +++ b/lib/discourse_dev/tag.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'rails' -require 'faker' +require "discourse_dev/record" +require "rails" +require "faker" module DiscourseDev class Tag < Record - def initialize super(::Tag, DiscourseDev.config.tag[:count]) end @@ -24,9 +23,7 @@ module DiscourseDev end def data - { - name: Faker::Discourse.unique.tag, - } + { name: Faker::Discourse.unique.tag } end end end diff --git a/lib/discourse_dev/topic.rb b/lib/discourse_dev/topic.rb index c08af628eb..9c6baaafd5 100644 --- a/lib/discourse_dev/topic.rb +++ b/lib/discourse_dev/topic.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true -require 'discourse_dev/record' -require 'faker' +require "discourse_dev/record" +require "faker" module DiscourseDev class Topic < Record - def initialize(private_messages: false, recipient: nil, ignore_current_count: false) @settings = DiscourseDev.config.topic @private_messages = private_messages @@ -33,15 +32,9 @@ module DiscourseDev end if @category - merge_attributes = { - category: @category.id, - tags: tags - } + merge_attributes = { category: @category.id, tags: tags } else - merge_attributes = { - archetype: "private_message", - target_usernames: [@recipient] - } + merge_attributes = { archetype: "private_message", target_usernames: [@recipient] } end { @@ -51,9 +44,11 @@ module DiscourseDev topic_opts: { import_mode: true, views: Faker::Number.between(from: 1, to: max_views), - custom_fields: { dev_sample: true } + custom_fields: { + dev_sample: true, + }, }, - skip_validations: true + skip_validations: true, }.merge(merge_attributes) end @@ -61,7 +56,10 @@ module DiscourseDev if current_count < I18n.t("faker.discourse.topics").count Faker::Discourse.unique.topic else - Faker::Lorem.unique.sentence(word_count: 5, supplemental: true, random_words_to_add: 4).chomp(".") + Faker::Lorem + .unique + .sentence(word_count: 5, supplemental: true, random_words_to_add: 4) + .chomp(".") end end @@ -70,9 +68,9 @@ module DiscourseDev @tags = [] - Faker::Number.between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)).times do - @tags << Faker::Discourse.tag - end + Faker::Number + .between(from: @settings.dig(:tags, :min), to: @settings.dig(:tags, :max)) + .times { @tags << Faker::Discourse.tag } @tags.uniq end @@ -92,7 +90,11 @@ module DiscourseDev if override = @settings.dig(:replies, :overrides).find { |o| o[:title] == topic_data[:title] } reply_count = override[:count] else - reply_count = Faker::Number.between(from: @settings.dig(:replies, :min), to: @settings.dig(:replies, :max)) + reply_count = + Faker::Number.between( + from: @settings.dig(:replies, :min), + to: @settings.dig(:replies, :max), + ) end topic = post.topic @@ -123,9 +125,7 @@ module DiscourseDev end def delete_unwanted_sidekiq_jobs - Sidekiq::ScheduledSet.new.each do |job| - job.delete if job.item["class"] == "Jobs::UserEmail" - end + Sidekiq::ScheduledSet.new.each { |job| job.delete if job.item["class"] == "Jobs::UserEmail" } end end end diff --git a/lib/discourse_diff.rb b/lib/discourse_diff.rb index 3887abe134..e31a699f85 100644 --- a/lib/discourse_diff.rb +++ b/lib/discourse_diff.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class DiscourseDiff - MAX_DIFFERENCE = 200 def initialize(before, after) @@ -9,8 +8,8 @@ class DiscourseDiff @after = after before_html = tokenize_html_blocks(@before) after_html = tokenize_html_blocks(@after) - before_markdown = tokenize_line(CGI::escapeHTML(@before)) - after_markdown = tokenize_line(CGI::escapeHTML(@after)) + before_markdown = tokenize_line(CGI.escapeHTML(@before)) + after_markdown = tokenize_line(CGI.escapeHTML(@after)) @block_by_block_diff = ONPDiff.new(before_html, after_html).paragraph_diff @line_by_line_diff = ONPDiff.new(before_markdown, after_markdown).short_diff @@ -21,7 +20,8 @@ class DiscourseDiff inline = [] while i < @block_by_block_diff.size op_code = @block_by_block_diff[i][1] - if op_code == :common then inline << @block_by_block_diff[i][0] + if op_code == :common + inline << @block_by_block_diff[i][0] else if op_code == :delete opposite_op_code = :add @@ -36,7 +36,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff inline << generate_inline_html(diff) i += 1 else @@ -73,7 +77,11 @@ class DiscourseDiff end if i + 1 < @block_by_block_diff.size && @block_by_block_diff[i + 1][1] == opposite_op_code - diff = ONPDiff.new(tokenize_html(@block_by_block_diff[first][0]), tokenize_html(@block_by_block_diff[second][0])).diff + diff = + ONPDiff.new( + tokenize_html(@block_by_block_diff[first][0]), + tokenize_html(@block_by_block_diff[second][0]), + ).diff deleted, inserted = generate_side_by_side_html(diff) left << deleted right << inserted @@ -109,9 +117,13 @@ class DiscourseDiff end if i + 1 < @line_by_line_diff.size && @line_by_line_diff[i + 1][1] == opposite_op_code - before_tokens, after_tokens = tokenize_markdown(@line_by_line_diff[first][0]), tokenize_markdown(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_markdown(@line_by_line_diff[first][0]), + tokenize_markdown(@line_by_line_diff[second][0]) if (before_tokens.size - after_tokens.size).abs > MAX_DIFFERENCE - before_tokens, after_tokens = tokenize_line(@line_by_line_diff[first][0]), tokenize_line(@line_by_line_diff[second][0]) + before_tokens, after_tokens = + tokenize_line(@line_by_line_diff[first][0]), + tokenize_line(@line_by_line_diff[second][0]) end diff = ONPDiff.new(before_tokens, after_tokens).short_diff deleted, inserted = generate_side_by_side_markdown(diff) @@ -178,7 +190,7 @@ class DiscourseDiff def add_class_or_wrap_in_tags(html_or_text, klass) result = html_or_text.dup index_of_next_chevron = result.index(">") - if result.size > 0 && result[0] == '<' && index_of_next_chevron + if result.size > 0 && result[0] == "<" && index_of_next_chevron index_of_class = result.index("class=") if index_of_class.nil? || index_of_class > index_of_next_chevron # we do not have a class for the current tag @@ -202,9 +214,12 @@ class DiscourseDiff inline = [] diff.each do |d| case d[1] - when :common then inline << d[0] - when :delete then inline << add_class_or_wrap_in_tags(d[0], "del") - when :add then inline << add_class_or_wrap_in_tags(d[0], "ins") + when :common + inline << d[0] + when :delete + inline << add_class_or_wrap_in_tags(d[0], "del") + when :add + inline << add_class_or_wrap_in_tags(d[0], "ins") end end inline @@ -217,8 +232,10 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << add_class_or_wrap_in_tags(d[0], "del") - when :add then inserted << add_class_or_wrap_in_tags(d[0], "ins") + when :delete + deleted << add_class_or_wrap_in_tags(d[0], "del") + when :add + inserted << add_class_or_wrap_in_tags(d[0], "ins") end end [deleted, inserted] @@ -231,15 +248,16 @@ class DiscourseDiff when :common deleted << d[0] inserted << d[0] - when :delete then deleted << "tags # When there's something else (