2
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2025-09-06 10:50:21 +08:00

Make rubocop happy again.

This commit is contained in:
Guo Xiang Tan 2018-06-07 13:28:18 +08:00
parent c6c1ef71c1
commit ad5082d969
79 changed files with 3155 additions and 3155 deletions

View file

@ -88,44 +88,44 @@ class Admin::BadgesController < Admin::AdminController
end end
private private
def find_badge def find_badge
params.require(:id) params.require(:id)
Badge.find(params[:id]) Badge.find(params[:id])
end end
# Options: # Options:
# :new - reset the badge id to nil before saving # :new - reset the badge id to nil before saving
def update_badge_from_params(badge, opts = {}) def update_badge_from_params(badge, opts = {})
errors = [] errors = []
Badge.transaction do Badge.transaction do
allowed = Badge.column_names.map(&:to_sym) allowed = Badge.column_names.map(&:to_sym)
allowed -= [:id, :created_at, :updated_at, :grant_count] allowed -= [:id, :created_at, :updated_at, :grant_count]
allowed -= Badge.protected_system_fields if badge.system? allowed -= Badge.protected_system_fields if badge.system?
allowed -= [:query] unless SiteSetting.enable_badge_sql allowed -= [:query] unless SiteSetting.enable_badge_sql
params.permit(*allowed) params.permit(*allowed)
allowed.each do |key| allowed.each do |key|
badge.send("#{key}=" , params[key]) if params[key] badge.send("#{key}=" , params[key]) if params[key]
end
# Badge query contract checks
begin
if SiteSetting.enable_badge_sql
BadgeGranter.contract_checks!(badge.query, target_posts: badge.target_posts, trigger: badge.trigger)
end
rescue => e
errors << e.message
raise ActiveRecord::Rollback
end
badge.id = nil if opts[:new]
badge.save!
end end
errors # Badge query contract checks
rescue ActiveRecord::RecordInvalid begin
errors.push(*badge.errors.full_messages) if SiteSetting.enable_badge_sql
errors BadgeGranter.contract_checks!(badge.query, target_posts: badge.target_posts, trigger: badge.trigger)
end
rescue => e
errors << e.message
raise ActiveRecord::Rollback
end
badge.id = nil if opts[:new]
badge.save!
end end
errors
rescue ActiveRecord::RecordInvalid
errors.push(*badge.errors.full_messages)
errors
end
end end

View file

@ -17,18 +17,18 @@ class Admin::EmbeddableHostsController < Admin::AdminController
protected protected
def save_host(host) def save_host(host)
host.host = params[:embeddable_host][:host] host.host = params[:embeddable_host][:host]
host.path_whitelist = params[:embeddable_host][:path_whitelist] host.path_whitelist = params[:embeddable_host][:path_whitelist]
host.class_name = params[:embeddable_host][:class_name] host.class_name = params[:embeddable_host][:class_name]
host.category_id = params[:embeddable_host][:category_id] host.category_id = params[:embeddable_host][:category_id]
host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank? host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank?
if host.save if host.save
render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true) render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true)
else else
render_json_error(host) render_json_error(host)
end
end end
end
end end

View file

@ -27,7 +27,7 @@ class Admin::EmbeddingController < Admin::AdminController
protected protected
def fetch_embedding def fetch_embedding
@embedding = Embedding.find @embedding = Embedding.find
end end
end end

View file

@ -51,13 +51,13 @@ class Admin::ScreenedIpAddressesController < Admin::AdminController
private private
def allowed_params def allowed_params
params.require(:ip_address) params.require(:ip_address)
params.permit(:ip_address, :action_name) params.permit(:ip_address, :action_name)
end end
def fetch_screened_ip_address def fetch_screened_ip_address
@screened_ip_address = ScreenedIpAddress.find(params[:id]) @screened_ip_address = ScreenedIpAddress.find(params[:id])
end end
end end

View file

@ -75,19 +75,19 @@ class Admin::SiteTextsController < Admin::AdminController
protected protected
def record_for(k, value = nil) def record_for(k, value = nil)
if k.ends_with?("_MF") if k.ends_with?("_MF")
ovr = TranslationOverride.where(translation_key: k, locale: I18n.locale).pluck(:value) ovr = TranslationOverride.where(translation_key: k, locale: I18n.locale).pluck(:value)
value = ovr[0] if ovr.present? value = ovr[0] if ovr.present?
end
value ||= I18n.t(k)
{ id: k, value: value }
end end
def find_site_text value ||= I18n.t(k)
raise Discourse::NotFound unless I18n.exists?(params[:id]) && !self.class.restricted_keys.include?(params[:id]) { id: k, value: value }
record_for(params[:id]) end
end
def find_site_text
raise Discourse::NotFound unless I18n.exists?(params[:id]) && !self.class.restricted_keys.include?(params[:id])
record_for(params[:id])
end
end end

View file

@ -223,59 +223,59 @@ class Admin::ThemesController < Admin::AdminController
private private
def update_default_theme def update_default_theme
if theme_params.key?(:default) if theme_params.key?(:default)
is_default = theme_params[:default].to_s == "true" is_default = theme_params[:default].to_s == "true"
if @theme.key == SiteSetting.default_theme_key && !is_default if @theme.key == SiteSetting.default_theme_key && !is_default
Theme.clear_default! Theme.clear_default!
elsif is_default elsif is_default
@theme.set_default! @theme.set_default!
end
end end
end end
end
def theme_params def theme_params
@theme_params ||= @theme_params ||=
begin begin
# deep munge is a train wreck, work around it for now # deep munge is a train wreck, work around it for now
params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids) params[:theme][:child_theme_ids] ||= [] if params[:theme].key?(:child_theme_ids)
params.require(:theme).permit( params.require(:theme).permit(
:name, :name,
:color_scheme_id, :color_scheme_id,
:default, :default,
:user_selectable, :user_selectable,
settings: {}, settings: {},
theme_fields: [:name, :target, :value, :upload_id, :type_id], theme_fields: [:name, :target, :value, :upload_id, :type_id],
child_theme_ids: [] child_theme_ids: []
)
end
end
def set_fields
return unless fields = theme_params[:theme_fields]
fields.each do |field|
@theme.set_field(
target: field[:target],
name: field[:name],
value: field[:value],
type_id: field[:type_id],
upload_id: field[:upload_id]
) )
end end
end end
def update_settings def set_fields
return unless target_settings = theme_params[:settings] return unless fields = theme_params[:theme_fields]
target_settings.each_pair do |setting_name, new_value| fields.each do |field|
@theme.update_setting(setting_name.to_sym, new_value) @theme.set_field(
end target: field[:target],
name: field[:name],
value: field[:value],
type_id: field[:type_id],
upload_id: field[:upload_id]
)
end end
end
def log_theme_change(old_record, new_record) def update_settings
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record) return unless target_settings = theme_params[:settings]
target_settings.each_pair do |setting_name, new_value|
@theme.update_setting(setting_name.to_sym, new_value)
end end
end
def log_theme_change(old_record, new_record)
StaffActionLogger.new(current_user).log_theme_change(old_record, new_record)
end
end end

View file

@ -47,11 +47,11 @@ class Admin::UserFieldsController < Admin::AdminController
protected protected
def update_options(field) def update_options(field)
options = params[:user_field][:options] options = params[:user_field][:options]
if options.present? if options.present?
UserFieldOption.where(user_field_id: field.id).delete_all UserFieldOption.where(user_field_id: field.id).delete_all
field.user_field_options_attributes = options.map { |o| { value: o } }.uniq field.user_field_options_attributes = options.map { |o| { value: o } }.uniq
end
end end
end
end end

View file

@ -544,34 +544,34 @@ class Admin::UsersController < Admin::AdminController
private private
def perform_post_action def perform_post_action
return unless params[:post_id].present? && return unless params[:post_id].present? &&
params[:post_action].present? params[:post_action].present?
if post = Post.where(id: params[:post_id]).first if post = Post.where(id: params[:post_id]).first
case params[:post_action] case params[:post_action]
when 'delete' when 'delete'
PostDestroyer.new(current_user, post).destroy PostDestroyer.new(current_user, post).destroy
when 'edit' when 'edit'
revisor = PostRevisor.new(post) revisor = PostRevisor.new(post)
# Take what the moderator edited in as gospel # Take what the moderator edited in as gospel
revisor.revise!( revisor.revise!(
current_user, current_user,
{ raw: params[:post_edit] }, { raw: params[:post_edit] },
skip_validations: true, skip_validations: true,
skip_revision: true skip_revision: true
) )
end
end end
end end
end
def fetch_user def fetch_user
@user = User.find_by(id: params[:user_id]) @user = User.find_by(id: params[:user_id])
end end
def refresh_browser(user) def refresh_browser(user)
MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id] MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id]
end end
end end

View file

@ -462,255 +462,255 @@ class ApplicationController < ActionController::Base
private private
def check_readonly_mode def check_readonly_mode
@readonly_mode = Discourse.readonly_mode? @readonly_mode = Discourse.readonly_mode?
end
def locale_from_header
begin
# Rails I18n uses underscores between the locale and the region; the request
# headers use hyphens.
require 'http_accept_language' unless defined? HttpAcceptLanguage
available_locales = I18n.available_locales.map { |locale| locale.to_s.tr('_', '-') }
parser = HttpAcceptLanguage::Parser.new(request.env["HTTP_ACCEPT_LANGUAGE"])
parser.language_region_compatible_from(available_locales).tr('-', '_')
rescue
# If Accept-Language headers are not set.
I18n.default_locale
end end
end
def locale_from_header def preload_anonymous_data
begin store_preloaded("site", Site.json_for(guardian))
# Rails I18n uses underscores between the locale and the region; the request store_preloaded("siteSettings", SiteSetting.client_settings_json)
# headers use hyphens. store_preloaded("themeSettings", Theme.settings_for_client(@theme_key))
require 'http_accept_language' unless defined? HttpAcceptLanguage store_preloaded("customHTML", custom_html_json)
available_locales = I18n.available_locales.map { |locale| locale.to_s.tr('_', '-') } store_preloaded("banner", banner_json)
parser = HttpAcceptLanguage::Parser.new(request.env["HTTP_ACCEPT_LANGUAGE"]) store_preloaded("customEmoji", custom_emoji)
parser.language_region_compatible_from(available_locales).tr('-', '_') store_preloaded("translationOverrides", I18n.client_overrides_json(I18n.locale))
rescue end
# If Accept-Language headers are not set.
I18n.default_locale
end
end
def preload_anonymous_data def preload_current_user_data
store_preloaded("site", Site.json_for(guardian)) store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false)))
store_preloaded("siteSettings", SiteSetting.client_settings_json) report = TopicTrackingState.report(current_user)
store_preloaded("themeSettings", Theme.settings_for_client(@theme_key)) serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
store_preloaded("customHTML", custom_html_json) store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
store_preloaded("banner", banner_json) end
store_preloaded("customEmoji", custom_emoji)
store_preloaded("translationOverrides", I18n.client_overrides_json(I18n.locale))
end
def preload_current_user_data def custom_html_json
store_preloaded("currentUser", MultiJson.dump(CurrentUserSerializer.new(current_user, scope: guardian, root: false))) target = view_context.mobile_view? ? :mobile : :desktop
report = TopicTrackingState.report(current_user)
serializer = ActiveModel::ArraySerializer.new(report, each_serializer: TopicTrackingStateSerializer)
store_preloaded("topicTrackingStates", MultiJson.dump(serializer))
end
def custom_html_json data =
target = view_context.mobile_view? ? :mobile : :desktop if @theme_key
{
data = top: Theme.lookup_field(@theme_key, target, "after_header"),
if @theme_key footer: Theme.lookup_field(@theme_key, target, "footer")
{ }
top: Theme.lookup_field(@theme_key, target, "after_header"),
footer: Theme.lookup_field(@theme_key, target, "footer")
}
else
{}
end
if DiscoursePluginRegistry.custom_html
data.merge! DiscoursePluginRegistry.custom_html
end
DiscoursePluginRegistry.html_builders.each do |name, _|
if name.start_with?("client:")
data[name.sub(/^client:/, '')] = DiscoursePluginRegistry.build_html(name, self)
end
end
MultiJson.dump(data)
end
def self.banner_json_cache
@banner_json_cache ||= DistributedCache.new("banner_json")
end
def banner_json
json = ApplicationController.banner_json_cache["json"]
unless json
topic = Topic.where(archetype: Archetype.banner).first
banner = topic.present? ? topic.banner : {}
ApplicationController.banner_json_cache["json"] = json = MultiJson.dump(banner)
end
json
end
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
# Render action for a JSON error.
#
# obj - a translated string, an ActiveRecord model, or an array of translated strings
# opts:
# type - a machine-readable description of the error
# status - HTTP status code to return
# headers - extra headers for the response
def render_json_error(obj, opts = {})
opts = { status: opts } if opts.is_a?(Integer)
opts.fetch(:headers, {}).each { |name, value| headers[name.to_s] = value }
render json: MultiJson.dump(create_errors_json(obj, opts)), status: opts[:status] || 422
end
def success_json
{ success: 'OK' }
end
def failed_json
{ failed: 'FAILED' }
end
def json_result(obj, opts = {})
if yield(obj)
json = success_json
# If we were given a serializer, add the class to the json that comes back
if opts[:serializer].present?
json[obj.class.name.underscore] = opts[:serializer].new(obj, scope: guardian).serializable_hash
end
render json: MultiJson.dump(json)
else else
error_obj = nil {}
if opts[:additional_errors] end
error_target = opts[:additional_errors].find do |o|
target = obj.send(o) if DiscoursePluginRegistry.custom_html
target && target.errors.present? data.merge! DiscoursePluginRegistry.custom_html
end end
error_obj = obj.send(error_target) if error_target
DiscoursePluginRegistry.html_builders.each do |name, _|
if name.start_with?("client:")
data[name.sub(/^client:/, '')] = DiscoursePluginRegistry.build_html(name, self)
end
end
MultiJson.dump(data)
end
def self.banner_json_cache
@banner_json_cache ||= DistributedCache.new("banner_json")
end
def banner_json
json = ApplicationController.banner_json_cache["json"]
unless json
topic = Topic.where(archetype: Archetype.banner).first
banner = topic.present? ? topic.banner : {}
ApplicationController.banner_json_cache["json"] = json = MultiJson.dump(banner)
end
json
end
def custom_emoji
serializer = ActiveModel::ArraySerializer.new(Emoji.custom, each_serializer: EmojiSerializer)
MultiJson.dump(serializer)
end
# Render action for a JSON error.
#
# obj - a translated string, an ActiveRecord model, or an array of translated strings
# opts:
# type - a machine-readable description of the error
# status - HTTP status code to return
# headers - extra headers for the response
def render_json_error(obj, opts = {})
opts = { status: opts } if opts.is_a?(Integer)
opts.fetch(:headers, {}).each { |name, value| headers[name.to_s] = value }
render json: MultiJson.dump(create_errors_json(obj, opts)), status: opts[:status] || 422
end
def success_json
{ success: 'OK' }
end
def failed_json
{ failed: 'FAILED' }
end
def json_result(obj, opts = {})
if yield(obj)
json = success_json
# If we were given a serializer, add the class to the json that comes back
if opts[:serializer].present?
json[obj.class.name.underscore] = opts[:serializer].new(obj, scope: guardian).serializable_hash
end
render json: MultiJson.dump(json)
else
error_obj = nil
if opts[:additional_errors]
error_target = opts[:additional_errors].find do |o|
target = obj.send(o)
target && target.errors.present?
end end
render_json_error(error_obj || obj) error_obj = obj.send(error_target) if error_target
end end
render_json_error(error_obj || obj)
end end
end
def mini_profiler_enabled? def mini_profiler_enabled?
defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?) defined?(Rack::MiniProfiler) && (guardian.is_developer? || Rails.env.development?)
end end
def authorize_mini_profiler def authorize_mini_profiler
return unless mini_profiler_enabled? return unless mini_profiler_enabled?
Rack::MiniProfiler.authorize_request Rack::MiniProfiler.authorize_request
end end
def check_xhr def check_xhr
# bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying # bypass xhr check on PUT / POST / DELETE provided api key is there, otherwise calling api is annoying
return if !request.get? && (is_api? || is_user_api?) return if !request.get? && (is_api? || is_user_api?)
raise RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?) raise RenderEmpty.new unless ((request.format && request.format.json?) || request.xhr?)
end end
def self.requires_login(arg = {}) def self.requires_login(arg = {})
@requires_login_arg = arg @requires_login_arg = arg
end end
def self.requires_login_arg def self.requires_login_arg
@requires_login_arg @requires_login_arg
end end
def block_if_requires_login def block_if_requires_login
if arg = self.class.requires_login_arg if arg = self.class.requires_login_arg
check = check =
if except = arg[:except] if except = arg[:except]
!except.include?(action_name.to_sym) !except.include?(action_name.to_sym)
elsif only = arg[:only] elsif only = arg[:only]
only.include?(action_name.to_sym) only.include?(action_name.to_sym)
else
true
end
ensure_logged_in if check
end
end
def ensure_logged_in
raise Discourse::NotLoggedIn.new unless current_user.present?
end
def ensure_staff
raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
end
def ensure_admin
raise Discourse::InvalidAccess.new unless current_user && current_user.admin?
end
def ensure_wizard_enabled
raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled?
end
def destination_url
request.original_url unless request.original_url =~ /uploads/
end
def redirect_to_login_if_required
return if current_user || (request.format.json? && is_api?)
if SiteSetting.login_required?
flash.keep
if SiteSetting.enable_sso?
# save original URL in a session so we can redirect after login
session[:destination_url] = destination_url
redirect_to path('/session/sso')
elsif params[:authComplete].present?
redirect_to path("/login?authComplete=true")
else else
# save original URL in a cookie (javascript redirects after login in this case) true
cookies[:destination_url] = destination_url
redirect_to path("/login")
end end
ensure_logged_in if check
end
end
def ensure_logged_in
raise Discourse::NotLoggedIn.new unless current_user.present?
end
def ensure_staff
raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
end
def ensure_admin
raise Discourse::InvalidAccess.new unless current_user && current_user.admin?
end
def ensure_wizard_enabled
raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled?
end
def destination_url
request.original_url unless request.original_url =~ /uploads/
end
def redirect_to_login_if_required
return if current_user || (request.format.json? && is_api?)
if SiteSetting.login_required?
flash.keep
if SiteSetting.enable_sso?
# save original URL in a session so we can redirect after login
session[:destination_url] = destination_url
redirect_to path('/session/sso')
elsif params[:authComplete].present?
redirect_to path("/login?authComplete=true")
else
# save original URL in a cookie (javascript redirects after login in this case)
cookies[:destination_url] = destination_url
redirect_to path("/login")
end end
end end
end
def block_if_readonly_mode def block_if_readonly_mode
return if request.fullpath.start_with?(path "/admin/backups") return if request.fullpath.start_with?(path "/admin/backups")
raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode
end
def build_not_found_page(status = 404, layout = false)
if SiteSetting.bootstrap_error_pages?
preload_json
layout = 'application' if layout == 'no_ember'
end end
def build_not_found_page(status = 404, layout = false) category_topic_ids = Category.pluck(:topic_id).compact
if SiteSetting.bootstrap_error_pages? @container_class = "wrap not-found-container"
preload_json @top_viewed = TopicQuery.new(nil, except_topic_ids: category_topic_ids).list_top_for("monthly").topics.first(10)
layout = 'application' if layout == 'no_ember' @recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10)
end @slug = params[:slug].class == String ? params[:slug] : ''
@slug = (params[:id].class == String ? params[:id] : '') if @slug.blank?
@slug.tr!('-', ' ')
@hide_google = true if SiteSetting.login_required
render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
end
category_topic_ids = Category.pluck(:topic_id).compact def is_asset_path
@container_class = "wrap not-found-container" request.env['DISCOURSE_IS_ASSET_PATH'] = 1
@top_viewed = TopicQuery.new(nil, except_topic_ids: category_topic_ids).list_top_for("monthly").topics.first(10) end
@recent = Topic.includes(:category).where.not(id: category_topic_ids).recent(10)
@slug = params[:slug].class == String ? params[:slug] : ''
@slug = (params[:id].class == String ? params[:id] : '') if @slug.blank?
@slug.tr!('-', ' ')
@hide_google = true if SiteSetting.login_required
render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
end
def is_asset_path
request.env['DISCOURSE_IS_ASSET_PATH'] = 1
end
protected protected
def render_post_json(post, add_raw = true) def render_post_json(post, add_raw = true)
post_serializer = PostSerializer.new(post, scope: guardian, root: false) post_serializer = PostSerializer.new(post, scope: guardian, root: false)
post_serializer.add_raw = add_raw post_serializer.add_raw = add_raw
counts = PostAction.counts_for([post], current_user) counts = PostAction.counts_for([post], current_user)
if counts && counts = counts[post.id] if counts && counts = counts[post.id]
post_serializer.post_actions = counts post_serializer.post_actions = counts
end
render_json_dump(post_serializer)
end end
render_json_dump(post_serializer)
end
# returns an array of integers given a param key # returns an array of integers given a param key
# returns nil if key is not found # returns nil if key is not found
def param_to_integer_list(key, delimiter = ',') def param_to_integer_list(key, delimiter = ',')
if params[key] if params[key]
params[key].split(delimiter).map(&:to_i) params[key].split(delimiter).map(&:to_i)
end
end end
end
end end

View file

@ -201,109 +201,109 @@ class CategoriesController < ApplicationController
end end
private private
def categories_and_topics(topics_filter) def categories_and_topics(topics_filter)
discourse_expires_in 1.minute discourse_expires_in 1.minute
category_options = { category_options = {
is_homepage: current_homepage == "categories".freeze, is_homepage: current_homepage == "categories".freeze,
parent_category_id: params[:parent_category_id], parent_category_id: params[:parent_category_id],
include_topics: false include_topics: false
} }
topic_options = { topic_options = {
per_page: SiteSetting.categories_topics, per_page: SiteSetting.categories_topics,
no_definitions: true, no_definitions: true,
exclude_category_ids: Category.where(suppress_from_latest: true).pluck(:id) exclude_category_ids: Category.where(suppress_from_latest: true).pluck(:id)
} }
result = CategoryAndTopicLists.new result = CategoryAndTopicLists.new
result.category_list = CategoryList.new(guardian, category_options) result.category_list = CategoryList.new(guardian, category_options)
if topics_filter == :latest if topics_filter == :latest
result.topic_list = TopicQuery.new(current_user, topic_options).list_latest result.topic_list = TopicQuery.new(current_user, topic_options).list_latest
elsif topics_filter == :top elsif topics_filter == :top
result.topic_list = TopicQuery.new(nil, topic_options).list_top_for(SiteSetting.top_page_default_timeframe.to_sym) result.topic_list = TopicQuery.new(nil, topic_options).list_top_for(SiteSetting.top_page_default_timeframe.to_sym)
end
draft_key = Draft::NEW_TOPIC
draft_sequence = DraftSequence.current(current_user, draft_key)
draft = Draft.get(current_user, draft_key, draft_sequence) if current_user
%w{category topic}.each do |type|
result.send(:"#{type}_list").draft = draft
result.send(:"#{type}_list").draft_key = draft_key
result.send(:"#{type}_list").draft_sequence = draft_sequence
end
render_serialized(result, CategoryAndTopicListsSerializer, root: false)
end
def required_param_keys
[:name, :color, :text_color]
end
def category_params
@category_params ||= begin
required_param_keys.each do |key|
params.require(key)
end end
draft_key = Draft::NEW_TOPIC if p = params[:permissions]
draft_sequence = DraftSequence.current(current_user, draft_key) p.each do |k, v|
draft = Draft.get(current_user, draft_key, draft_sequence) if current_user p[k] = v.to_i
end
%w{category topic}.each do |type|
result.send(:"#{type}_list").draft = draft
result.send(:"#{type}_list").draft_key = draft_key
result.send(:"#{type}_list").draft_sequence = draft_sequence
end end
render_serialized(result, CategoryAndTopicListsSerializer, root: false) if SiteSetting.tagging_enabled
end params[:allowed_tags] ||= []
params[:allowed_tag_groups] ||= []
def required_param_keys
[:name, :color, :text_color]
end
def category_params
@category_params ||= begin
required_param_keys.each do |key|
params.require(key)
end
if p = params[:permissions]
p.each do |k, v|
p[k] = v.to_i
end
end
if SiteSetting.tagging_enabled
params[:allowed_tags] ||= []
params[:allowed_tag_groups] ||= []
end
params.permit(*required_param_keys,
:position,
:email_in,
:email_in_allow_strangers,
:mailinglist_mirror,
:suppress_from_latest,
:all_topics_wiki,
:parent_category_id,
:auto_close_hours,
:auto_close_based_on_last_post,
:uploaded_logo_id,
:uploaded_background_id,
:slug,
:allow_badges,
:topic_template,
:sort_order,
:sort_ascending,
:topic_featured_link_allowed,
:show_subcategory_list,
:num_featured_topics,
:default_view,
:subcategory_list_style,
:default_top_period,
:minimum_required_tags,
custom_fields: [params[:custom_fields].try(:keys)],
permissions: [*p.try(:keys)],
allowed_tags: [],
allowed_tag_groups: [])
end end
end
def fetch_category params.permit(*required_param_keys,
@category = Category.find_by(slug: params[:id]) || Category.find_by(id: params[:id].to_i) :position,
:email_in,
:email_in_allow_strangers,
:mailinglist_mirror,
:suppress_from_latest,
:all_topics_wiki,
:parent_category_id,
:auto_close_hours,
:auto_close_based_on_last_post,
:uploaded_logo_id,
:uploaded_background_id,
:slug,
:allow_badges,
:topic_template,
:sort_order,
:sort_ascending,
:topic_featured_link_allowed,
:show_subcategory_list,
:num_featured_topics,
:default_view,
:subcategory_list_style,
:default_top_period,
:minimum_required_tags,
custom_fields: [params[:custom_fields].try(:keys)],
permissions: [*p.try(:keys)],
allowed_tags: [],
allowed_tag_groups: [])
end end
end
def initialize_staff_action_logger def fetch_category
@staff_action_logger = StaffActionLogger.new(current_user) @category = Category.find_by(slug: params[:id]) || Category.find_by(id: params[:id].to_i)
end end
def include_topics(parent_category = nil) def initialize_staff_action_logger
style = SiteSetting.desktop_category_page_style @staff_action_logger = StaffActionLogger.new(current_user)
view_context.mobile_view? || end
params[:include_topics] ||
(parent_category && parent_category.subcategory_list_includes_topics?) || def include_topics(parent_category = nil)
style == "categories_with_featured_topics".freeze || style = SiteSetting.desktop_category_page_style
style == "categories_with_top_topics".freeze view_context.mobile_view? ||
end params[:include_topics] ||
(parent_category && parent_category.subcategory_list_includes_topics?) ||
style == "categories_with_featured_topics".freeze ||
style == "categories_with_top_topics".freeze
end
end end

View file

@ -31,9 +31,9 @@ class ClicksController < ApplicationController
private private
def track_params def track_params
params.require(:url) params.require(:url)
params.permit(:url, :post_id, :topic_id, :redirect) params.permit(:url, :post_id, :topic_id, :redirect)
end end
end end

View file

@ -92,28 +92,28 @@ class EmbedController < ApplicationController
private private
def get_embeddable_css_class def get_embeddable_css_class
@embeddable_css_class = "" @embeddable_css_class = ""
embeddable_host = EmbeddableHost.record_for_url(request.referer) embeddable_host = EmbeddableHost.record_for_url(request.referer)
@embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present? @embeddable_css_class = " class=\"#{embeddable_host.class_name}\"" if embeddable_host.present? && embeddable_host.class_name.present?
end end
def ensure_api_request def ensure_api_request
raise Discourse::InvalidAccess.new('api key not set') if !is_api? raise Discourse::InvalidAccess.new('api key not set') if !is_api?
end end
def ensure_embeddable def ensure_embeddable
if !(Rails.env.development? && current_user&.admin?) if !(Rails.env.development? && current_user&.admin?)
referer = request.referer referer = request.referer
unless referer && EmbeddableHost.url_allowed?(referer) unless referer && EmbeddableHost.url_allowed?(referer)
raise Discourse::InvalidAccess.new('invalid referer host') raise Discourse::InvalidAccess.new('invalid referer host')
end
end end
response.headers['X-Frame-Options'] = "ALLOWALL"
rescue URI::InvalidURIError
raise Discourse::InvalidAccess.new('invalid referer host')
end end
response.headers['X-Frame-Options'] = "ALLOWALL"
rescue URI::InvalidURIError
raise Discourse::InvalidAccess.new('invalid referer host')
end
end end

View file

@ -14,8 +14,8 @@ class ExceptionsController < ApplicationController
private private
def hide_google def hide_google
@hide_google = true if SiteSetting.login_required @hide_google = true if SiteSetting.login_required
end end
end end

View file

@ -9,10 +9,10 @@ class ExportCsvController < ApplicationController
end end
private private
def export_params def export_params
@_export_params ||= begin @_export_params ||= begin
params.require(:entity) params.require(:entity)
params.permit(:entity, args: [:name, :start_date, :end_date, :category_id, :group_id, :trust_level]).to_h params.permit(:entity, args: [:name, :start_date, :end_date, :category_id, :group_id, :trust_level]).to_h
end
end end
end
end end

View file

@ -48,18 +48,18 @@ class FinishInstallationController < ApplicationController
protected protected
def redirect_confirm(email) def redirect_confirm(email)
session[:registered_email] = email session[:registered_email] = email
redirect_to(finish_installation_confirm_email_path) redirect_to(finish_installation_confirm_email_path)
end end
def find_allowed_emails def find_allowed_emails
return [] unless GlobalSetting.respond_to?(:developer_emails) && GlobalSetting.developer_emails.present? return [] unless GlobalSetting.respond_to?(:developer_emails) && GlobalSetting.developer_emails.present?
GlobalSetting.developer_emails.split(",").map(&:strip) GlobalSetting.developer_emails.split(",").map(&:strip)
end end
def ensure_no_admins def ensure_no_admins
preload_anonymous_data preload_anonymous_data
raise Discourse::InvalidAccess.new unless SiteSetting.has_login_hint? raise Discourse::InvalidAccess.new unless SiteSetting.has_login_hint?
end end
end end

View file

@ -211,14 +211,14 @@ class InvitesController < ApplicationController
private private
def post_process_invite(user) def post_process_invite(user)
user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message
if user.has_password? if user.has_password?
email_token = user.email_tokens.create(email: user.email) email_token = user.email_tokens.create(email: user.email)
Jobs.enqueue(:critical_user_email, type: :signup, user_id: user.id, email_token: email_token.token) Jobs.enqueue(:critical_user_email, type: :signup, user_id: user.id, email_token: email_token.token)
elsif !SiteSetting.enable_sso && SiteSetting.enable_local_logins elsif !SiteSetting.enable_sso && SiteSetting.enable_local_logins
Jobs.enqueue(:invite_password_instructions_email, username: user.username) Jobs.enqueue(:invite_password_instructions_email, username: user.username)
end
end end
end
end end

View file

@ -84,16 +84,16 @@ class NotificationsController < ApplicationController
private private
def set_notification def set_notification
@notification = Notification.find(params[:id]) @notification = Notification.find(params[:id])
end end
def notification_params def notification_params
params.permit(:notification_type, :user_id, :data, :read, :topic_id, :post_number, :post_action_id) params.permit(:notification_type, :user_id, :data, :read, :topic_id, :post_number, :post_action_id)
end end
def render_notification def render_notification
render_json_dump(NotificationSerializer.new(@notification, scope: guardian, root: false)) render_json_dump(NotificationSerializer.new(@notification, scope: guardian, root: false))
end end
end end

View file

@ -65,32 +65,32 @@ class PostActionsController < ApplicationController
private private
def fetch_post_from_params def fetch_post_from_params
params.require(:id) params.require(:id)
flag_topic = params[:flag_topic] flag_topic = params[:flag_topic]
flag_topic = flag_topic && (flag_topic == true || flag_topic == "true") flag_topic = flag_topic && (flag_topic == true || flag_topic == "true")
post_id = if flag_topic post_id = if flag_topic
begin begin
Topic.find(params[:id]).posts.first.id Topic.find(params[:id]).posts.first.id
rescue rescue
raise Discourse::NotFound raise Discourse::NotFound
end
else
params[:id]
end end
else
finder = Post.where(id: post_id) params[:id]
# Include deleted posts if the user is a staff
finder = finder.with_deleted if guardian.is_staff?
@post = finder.first
end end
def fetch_post_action_type_id_from_params finder = Post.where(id: post_id)
params.require(:post_action_type_id)
@post_action_type_id = params[:post_action_type_id].to_i # Include deleted posts if the user is a staff
end finder = finder.with_deleted if guardian.is_staff?
@post = finder.first
end
def fetch_post_action_type_id_from_params
params.require(:post_action_type_id)
@post_action_type_id = params[:post_action_type_id].to_i
end
end end

View file

@ -52,18 +52,18 @@ class QueuedPostsController < ApplicationController
private private
def user_deletion_opts def user_deletion_opts
base = { base = {
context: I18n.t('queue.delete_reason', performed_by: current_user.username), context: I18n.t('queue.delete_reason', performed_by: current_user.username),
delete_posts: true, delete_posts: true,
delete_as_spammer: true delete_as_spammer: true
} }
if Rails.env.production? && ENV["Staging"].nil? if Rails.env.production? && ENV["Staging"].nil?
base.merge!(block_email: true, block_ip: true) base.merge!(block_email: true, block_ip: true)
end
base
end end
base
end
end end

View file

@ -98,11 +98,11 @@ class StylesheetsController < ApplicationController
private private
def read_file(location) def read_file(location)
begin begin
File.read(location) File.read(location)
rescue Errno::ENOENT rescue Errno::ENOENT
end
end end
end
end end

View file

@ -64,28 +64,28 @@ class TagGroupsController < ApplicationController
private private
def fetch_tag_group def fetch_tag_group
@tag_group = TagGroup.find(params[:id]) @tag_group = TagGroup.find(params[:id])
end end
def tag_groups_params def tag_groups_params
if permissions = params[:permissions] if permissions = params[:permissions]
permissions.each do |k, v| permissions.each do |k, v|
permissions[k] = v.to_i permissions[k] = v.to_i
end
end end
result = params.permit(
:id,
:name,
:one_per_topic,
tag_names: [],
parent_tag_name: [],
permissions: permissions&.keys,
)
result[:tag_names] ||= []
result[:parent_tag_name] ||= []
result[:one_per_topic] = (params[:one_per_topic] == "true")
result
end end
result = params.permit(
:id,
:name,
:one_per_topic,
tag_names: [],
parent_tag_name: [],
permissions: permissions&.keys,
)
result[:tag_names] ||= []
result[:parent_tag_name] ||= []
result[:one_per_topic] = (params[:one_per_topic] == "true")
result
end
end end

View file

@ -207,130 +207,130 @@ class TagsController < ::ApplicationController
private private
def ensure_tags_enabled def ensure_tags_enabled
raise Discourse::NotFound unless SiteSetting.tagging_enabled? raise Discourse::NotFound unless SiteSetting.tagging_enabled?
end end
def self.tag_counts_json(tags) def self.tag_counts_json(tags)
tags.map { |t| { id: t.name, text: t.name, count: t.topic_count, pm_count: t.pm_topic_count } } tags.map { |t| { id: t.name, text: t.name, count: t.topic_count, pm_count: t.pm_topic_count } }
end end
def set_category_from_params def set_category_from_params
slug_or_id = params[:category] slug_or_id = params[:category]
return true if slug_or_id.nil? return true if slug_or_id.nil?
if slug_or_id == 'none' && params[:parent_category] if slug_or_id == 'none' && params[:parent_category]
@filter_on_category = Category.query_category(params[:parent_category], nil) @filter_on_category = Category.query_category(params[:parent_category], nil)
params[:no_subcategories] = 'true' params[:no_subcategories] = 'true'
else else
parent_slug_or_id = params[:parent_category] parent_slug_or_id = params[:parent_category]
parent_category_id = nil parent_category_id = nil
if parent_slug_or_id.present? if parent_slug_or_id.present?
parent_category_id = Category.query_parent_category(parent_slug_or_id) parent_category_id = Category.query_parent_category(parent_slug_or_id)
category_redirect_or_not_found && (return) if parent_category_id.blank? category_redirect_or_not_found && (return) if parent_category_id.blank?
end
@filter_on_category = Category.query_category(slug_or_id, parent_category_id)
end end
category_redirect_or_not_found && (return) if !@filter_on_category @filter_on_category = Category.query_category(slug_or_id, parent_category_id)
guardian.ensure_can_see!(@filter_on_category)
end end
# TODO: this is duplication of ListController category_redirect_or_not_found && (return) if !@filter_on_category
def page_params(opts = nil)
opts ||= {}
route_params = { format: 'json' }
route_params[:category] = @filter_on_category.slug_for_url if @filter_on_category
route_params[:parent_category] = @filter_on_category.parent_category.slug_for_url if @filter_on_category && @filter_on_category.parent_category
route_params[:order] = opts[:order] if opts[:order].present?
route_params[:ascending] = opts[:ascending] if opts[:ascending].present?
route_params
end
def next_page_params(opts = nil) guardian.ensure_can_see!(@filter_on_category)
page_params(opts).merge(page: params[:page].to_i + 1) end
end
def prev_page_params(opts = nil) # TODO: this is duplication of ListController
pg = params[:page].to_i def page_params(opts = nil)
if pg > 1 opts ||= {}
page_params(opts).merge(page: pg - 1) route_params = { format: 'json' }
else route_params[:category] = @filter_on_category.slug_for_url if @filter_on_category
page_params(opts).merge(page: nil) route_params[:parent_category] = @filter_on_category.parent_category.slug_for_url if @filter_on_category && @filter_on_category.parent_category
route_params[:order] = opts[:order] if opts[:order].present?
route_params[:ascending] = opts[:ascending] if opts[:ascending].present?
route_params
end
def next_page_params(opts = nil)
page_params(opts).merge(page: params[:page].to_i + 1)
end
def prev_page_params(opts = nil)
pg = params[:page].to_i
if pg > 1
page_params(opts).merge(page: pg - 1)
else
page_params(opts).merge(page: nil)
end
end
def url_method(opts = {})
if opts[:parent_category] && opts[:category]
"tag_parent_category_category_#{action_name}_path"
elsif opts[:category]
"tag_category_#{action_name}_path"
else
"tag_#{action_name}_path"
end
end
def construct_url_with(action, opts)
method = url_method(opts)
begin
url = if action == :prev
public_send(method, opts.merge(prev_page_params(opts)))
else # :next
public_send(method, opts.merge(next_page_params(opts)))
end end
rescue ActionController::UrlGenerationError
raise Discourse::NotFound
end
url.sub('.json?', '?')
end
def build_topic_list_options
options = {
page: params[:page],
topic_ids: param_to_integer_list(:topic_ids),
exclude_category_ids: params[:exclude_category_ids],
category: @filter_on_category ? @filter_on_category.id : params[:category],
order: params[:order],
ascending: params[:ascending],
min_posts: params[:min_posts],
max_posts: params[:max_posts],
status: params[:status],
filter: params[:filter],
state: params[:state],
search: params[:search],
q: params[:q]
}
options[:no_subcategories] = true if params[:no_subcategories] == 'true'
options[:slow_platform] = true if slow_platform?
if params[:tag_id] == 'none'
options[:no_tags] = true
else
options[:tags] = tag_params
options[:match_all_tags] = true
end end
def url_method(opts = {}) options
if opts[:parent_category] && opts[:category] end
"tag_parent_category_category_#{action_name}_path"
elsif opts[:category] def category_redirect_or_not_found
"tag_category_#{action_name}_path" # automatic redirects for renamed categories
else url = params[:parent_category] ? "c/#{params[:parent_category]}/#{params[:category]}" : "c/#{params[:category]}"
"tag_#{action_name}_path" permalink = Permalink.find_by_url(url)
end
if permalink.present? && permalink.category_id
redirect_to "#{Discourse::base_uri}/tags#{permalink.target_url}/#{params[:tag_id]}", status: :moved_permanently
else
# redirect to 404
raise Discourse::NotFound
end end
end
def construct_url_with(action, opts) def tag_params
method = url_method(opts) [@tag_id].concat(Array(@additional_tags))
end
begin
url = if action == :prev
public_send(method, opts.merge(prev_page_params(opts)))
else # :next
public_send(method, opts.merge(next_page_params(opts)))
end
rescue ActionController::UrlGenerationError
raise Discourse::NotFound
end
url.sub('.json?', '?')
end
def build_topic_list_options
options = {
page: params[:page],
topic_ids: param_to_integer_list(:topic_ids),
exclude_category_ids: params[:exclude_category_ids],
category: @filter_on_category ? @filter_on_category.id : params[:category],
order: params[:order],
ascending: params[:ascending],
min_posts: params[:min_posts],
max_posts: params[:max_posts],
status: params[:status],
filter: params[:filter],
state: params[:state],
search: params[:search],
q: params[:q]
}
options[:no_subcategories] = true if params[:no_subcategories] == 'true'
options[:slow_platform] = true if slow_platform?
if params[:tag_id] == 'none'
options[:no_tags] = true
else
options[:tags] = tag_params
options[:match_all_tags] = true
end
options
end
def category_redirect_or_not_found
# automatic redirects for renamed categories
url = params[:parent_category] ? "c/#{params[:parent_category]}/#{params[:category]}" : "c/#{params[:category]}"
permalink = Permalink.find_by_url(url)
if permalink.present? && permalink.category_id
redirect_to "#{Discourse::base_uri}/tags#{permalink.target_url}/#{params[:tag_id]}", status: :moved_permanently
else
# redirect to 404
raise Discourse::NotFound
end
end
def tag_params
[@tag_id].concat(Array(@additional_tags))
end
end end

View file

@ -92,28 +92,28 @@ class UserBadgesController < ApplicationController
private private
# Get the badge from either the badge name or id specified in the params. # Get the badge from either the badge name or id specified in the params.
def fetch_badge_from_params def fetch_badge_from_params
badge = nil badge = nil
params.permit(:badge_name) params.permit(:badge_name)
if params[:badge_name].nil? if params[:badge_name].nil?
params.require(:badge_id) params.require(:badge_id)
badge = Badge.find_by(id: params[:badge_id], enabled: true) badge = Badge.find_by(id: params[:badge_id], enabled: true)
else else
badge = Badge.find_by(name: params[:badge_name], enabled: true) badge = Badge.find_by(name: params[:badge_name], enabled: true)
end
raise Discourse::NotFound if badge.blank?
badge
end end
raise Discourse::NotFound if badge.blank?
def can_assign_badge_to_user?(user) badge
master_api_call = current_user.nil? && is_api? end
master_api_call || guardian.can_grant_badges?(user)
end
def ensure_badges_enabled def can_assign_badge_to_user?(user)
raise Discourse::NotFound unless SiteSetting.enable_badges? master_api_call = current_user.nil? && is_api?
end master_api_call || guardian.can_grant_badges?(user)
end
def ensure_badges_enabled
raise Discourse::NotFound unless SiteSetting.enable_badges?
end
end end

View file

@ -994,104 +994,104 @@ class UsersController < ApplicationController
private private
def honeypot_value def honeypot_value
Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{GlobalSetting.safe_secret_key_base}")[0, 15] Digest::SHA1::hexdigest("#{Discourse.current_hostname}:#{GlobalSetting.safe_secret_key_base}")[0, 15]
end
def challenge_value
challenge = $redis.get('SECRET_CHALLENGE')
unless challenge && challenge.length == 16 * 2
challenge = SecureRandom.hex(16)
$redis.set('SECRET_CHALLENGE', challenge)
end end
def challenge_value challenge
challenge = $redis.get('SECRET_CHALLENGE') end
unless challenge && challenge.length == 16 * 2
challenge = SecureRandom.hex(16)
$redis.set('SECRET_CHALLENGE', challenge)
end
challenge def respond_to_suspicious_request
if suspicious?(params)
render json: {
success: true,
active: false,
message: I18n.t("login.activate_email", email: params[:email])
}
end
end
def suspicious?(params)
return false if current_user && is_api? && current_user.admin?
honeypot_or_challenge_fails?(params) || SiteSetting.invite_only?
end
def honeypot_or_challenge_fails?(params)
return false if is_api?
params[:password_confirmation] != honeypot_value ||
params[:challenge] != challenge_value.try(:reverse)
end
def user_params
permitted = [
:name,
:email,
:password,
:username,
:title,
:date_of_birth,
:muted_usernames,
:theme_key,
:locale,
:bio_raw,
:location,
:website,
:dismissed_banner_key,
:profile_background,
:card_background
]
permitted.concat UserUpdater::OPTION_ATTR
permitted.concat UserUpdater::CATEGORY_IDS.keys.map { |k| { k => [] } }
permitted.concat UserUpdater::TAG_NAMES.keys
result = params
.permit(permitted)
.reverse_merge(
ip_address: request.remote_ip,
registration_ip_address: request.remote_ip,
locale: user_locale
)
if !UsernameCheckerService.is_developer?(result['email']) &&
is_api? &&
current_user.present? &&
current_user.admin?
result.merge!(params.permit(:active, :staged, :approved))
end end
def respond_to_suspicious_request modify_user_params(result)
if suspicious?(params) end
render json: {
success: true, # Plugins can use this to modify user parameters
active: false, def modify_user_params(attrs)
message: I18n.t("login.activate_email", email: params[:email]) attrs
} end
end
end def user_locale
I18n.locale
def suspicious?(params) end
return false if current_user && is_api? && current_user.admin?
honeypot_or_challenge_fails?(params) || SiteSetting.invite_only? def fail_with(key)
end render json: { success: false, message: I18n.t(key) }
end
def honeypot_or_challenge_fails?(params)
return false if is_api? def track_visit_to_user_profile
params[:password_confirmation] != honeypot_value || user_profile_id = @user.user_profile.id
params[:challenge] != challenge_value.try(:reverse) ip = request.remote_ip
end user_id = (current_user.id if current_user)
def user_params Scheduler::Defer.later 'Track profile view visit' do
permitted = [ UserProfileView.add(user_profile_id, ip, user_id)
:name,
:email,
:password,
:username,
:title,
:date_of_birth,
:muted_usernames,
:theme_key,
:locale,
:bio_raw,
:location,
:website,
:dismissed_banner_key,
:profile_background,
:card_background
]
permitted.concat UserUpdater::OPTION_ATTR
permitted.concat UserUpdater::CATEGORY_IDS.keys.map { |k| { k => [] } }
permitted.concat UserUpdater::TAG_NAMES.keys
result = params
.permit(permitted)
.reverse_merge(
ip_address: request.remote_ip,
registration_ip_address: request.remote_ip,
locale: user_locale
)
if !UsernameCheckerService.is_developer?(result['email']) &&
is_api? &&
current_user.present? &&
current_user.admin?
result.merge!(params.permit(:active, :staged, :approved))
end
modify_user_params(result)
end
# Plugins can use this to modify user parameters
def modify_user_params(attrs)
attrs
end
def user_locale
I18n.locale
end
def fail_with(key)
render json: { success: false, message: I18n.t(key) }
end
def track_visit_to_user_profile
user_profile_id = @user.user_profile.id
ip = request.remote_ip
user_id = (current_user.id if current_user)
Scheduler::Defer.later 'Track profile view visit' do
UserProfileView.add(user_profile_id, ip, user_id)
end
end end
end
end end

View file

@ -119,30 +119,30 @@ class WebhooksController < ActionController::Base
private private
def mailgun_failure def mailgun_failure
render body: nil, status: 406 render body: nil, status: 406
end end
def mailgun_success def mailgun_success
render body: nil, status: 200 render body: nil, status: 200
end end
def mailgun_verify(timestamp, token, signature) def mailgun_verify(timestamp, token, signature)
digest = OpenSSL::Digest::SHA256.new digest = OpenSSL::Digest::SHA256.new
data = "#{timestamp}#{token}" data = "#{timestamp}#{token}"
signature == OpenSSL::HMAC.hexdigest(digest, SiteSetting.mailgun_api_key, data) signature == OpenSSL::HMAC.hexdigest(digest, SiteSetting.mailgun_api_key, data)
end end
def process_bounce(message_id, to_address, bounce_score) def process_bounce(message_id, to_address, bounce_score)
return if message_id.blank? || to_address.blank? return if message_id.blank? || to_address.blank?
email_log = EmailLog.find_by(message_id: message_id, to_address: to_address) email_log = EmailLog.find_by(message_id: message_id, to_address: to_address)
return if email_log.nil? return if email_log.nil?
email_log.update_columns(bounced: true) email_log.update_columns(bounced: true)
return if email_log.user.nil? || email_log.user.email.blank? return if email_log.user.nil? || email_log.user.email.blank?
Email::Receiver.update_bounce_score(email_log.user.email, bounce_score) Email::Receiver.update_bounce_score(email_log.user.email, bounce_score)
end end
end end

View file

@ -211,169 +211,169 @@ module Jobs
private private
def escape_comma(string) def escape_comma(string)
if string && string =~ /,/ if string && string =~ /,/
return "#{string}" return "#{string}"
else else
return string return string
end
end
def get_base_user_array(user)
user_array = []
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced_till, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views)
end
def add_single_sign_on(user, user_info_array)
if user.single_sign_on_record
user_info_array.push(user.single_sign_on_record.external_id, user.single_sign_on_record.external_email, user.single_sign_on_record.external_username, escape_comma(user.single_sign_on_record.external_name), user.single_sign_on_record.external_avatar_url)
else
user_info_array.push(nil, nil, nil, nil, nil)
end
user_info_array
end
def add_custom_fields(user, user_info_array, user_field_ids)
if user_field_ids.present?
user.user_fields.each do |custom_field|
user_info_array << escape_comma(custom_field[1])
end end
end end
user_info_array
end
def get_base_user_array(user) def add_group_names(user, user_info_array)
user_array = [] group_names = user.groups.each_with_object("") do |group, names|
user_array.push(user.id, escape_comma(user.name), user.username, user.email, escape_comma(user.title), user.created_at, user.last_seen_at, user.last_posted_at, user.last_emailed_at, user.trust_level, user.approved, user.suspended_at, user.suspended_till, user.silenced_till, user.active, user.admin, user.moderator, user.ip_address, user.staged, user.user_stat.topics_entered, user.user_stat.posts_read_count, user.user_stat.time_read, user.user_stat.topic_count, user.user_stat.post_count, user.user_stat.likes_given, user.user_stat.likes_received, escape_comma(user.user_profile.location), user.user_profile.website, user.user_profile.views) names << "#{group.name};"
end end
user_info_array << group_names[0..-2] unless group_names.blank?
group_names = nil
user_info_array
end
def add_single_sign_on(user, user_info_array) def get_user_archive_fields(user_archive)
if user.single_sign_on_record user_archive_array = []
user_info_array.push(user.single_sign_on_record.external_id, user.single_sign_on_record.external_email, user.single_sign_on_record.external_username, escape_comma(user.single_sign_on_record.external_name), user.single_sign_on_record.external_avatar_url) topic_data = user_archive.topic
else user_archive = user_archive.as_json
user_info_array.push(nil, nil, nil, nil, nil) topic_data = Topic.with_deleted.find_by(id: user_archive['topic_id']) if topic_data.nil?
end return user_archive_array if topic_data.nil?
user_info_array category = topic_data.category
end sub_category_name = "-"
if category
def add_custom_fields(user, user_info_array, user_field_ids) category_name = category.name
if user_field_ids.present? if category.parent_category_id.present?
user.user_fields.each do |custom_field| # sub category
user_info_array << escape_comma(custom_field[1]) if parent_category = Category.find_by(id: category.parent_category_id)
category_name = parent_category.name
sub_category_name = category.name
end end
end end
user_info_array else
# PM
category_name = "-"
end
is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no")
url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}"
topic_hash = { "post" => user_archive['raw'], "topic_title" => topic_data.title, "category" => category_name, "sub_category" => sub_category_name, "is_pm" => is_pm, "url" => url }
user_archive.merge!(topic_hash)
HEADER_ATTRS_FOR['user_archive'].each do |attr|
user_archive_array.push(user_archive[attr])
end end
def add_group_names(user, user_info_array) user_archive_array
group_names = user.groups.each_with_object("") do |group, names| end
names << "#{group.name};"
end
user_info_array << group_names[0..-2] unless group_names.blank?
group_names = nil
user_info_array
end
def get_user_archive_fields(user_archive) def get_staff_action_fields(staff_action)
user_archive_array = [] staff_action_array = []
topic_data = user_archive.topic
user_archive = user_archive.as_json
topic_data = Topic.with_deleted.find_by(id: user_archive['topic_id']) if topic_data.nil?
return user_archive_array if topic_data.nil?
category = topic_data.category
sub_category_name = "-"
if category
category_name = category.name
if category.parent_category_id.present?
# sub category
if parent_category = Category.find_by(id: category.parent_category_id)
category_name = parent_category.name
sub_category_name = category.name
end
end
else
# PM
category_name = "-"
end
is_pm = topic_data.archetype == "private_message" ? I18n.t("csv_export.boolean_yes") : I18n.t("csv_export.boolean_no")
url = "#{Discourse.base_url}/t/#{topic_data.slug}/#{topic_data.id}/#{user_archive['post_number']}"
topic_hash = { "post" => user_archive['raw'], "topic_title" => topic_data.title, "category" => category_name, "sub_category" => sub_category_name, "is_pm" => is_pm, "url" => url } HEADER_ATTRS_FOR['staff_action'].each do |attr|
user_archive.merge!(topic_hash) data =
if attr == 'action'
HEADER_ATTRS_FOR['user_archive'].each do |attr| UserHistory.actions.key(staff_action.attributes[attr]).to_s
user_archive_array.push(user_archive[attr]) elsif attr == 'staff_user'
end user = User.find_by(id: staff_action.attributes['acting_user_id'])
user.username if !user.nil?
user_archive_array elsif attr == 'subject'
end user = User.find_by(id: staff_action.attributes['target_user_id'])
user.nil? ? staff_action.attributes[attr] : "#{user.username} #{staff_action.attributes[attr]}"
def get_staff_action_fields(staff_action)
staff_action_array = []
HEADER_ATTRS_FOR['staff_action'].each do |attr|
data =
if attr == 'action'
UserHistory.actions.key(staff_action.attributes[attr]).to_s
elsif attr == 'staff_user'
user = User.find_by(id: staff_action.attributes['acting_user_id'])
user.username if !user.nil?
elsif attr == 'subject'
user = User.find_by(id: staff_action.attributes['target_user_id'])
user.nil? ? staff_action.attributes[attr] : "#{user.username} #{staff_action.attributes[attr]}"
else
staff_action.attributes[attr]
end
staff_action_array.push(data)
end
staff_action_array
end
def get_screened_email_fields(screened_email)
screened_email_array = []
HEADER_ATTRS_FOR['screened_email'].each do |attr|
data =
if attr == 'action'
ScreenedEmail.actions.key(screened_email.attributes['action_type']).to_s
else
screened_email.attributes[attr]
end
screened_email_array.push(data)
end
screened_email_array
end
def get_screened_ip_fields(screened_ip)
screened_ip_array = []
HEADER_ATTRS_FOR['screened_ip'].each do |attr|
data =
if attr == 'action'
ScreenedIpAddress.actions.key(screened_ip.attributes['action_type']).to_s
else
screened_ip.attributes[attr]
end
screened_ip_array.push(data)
end
screened_ip_array
end
def get_screened_url_fields(screened_url)
screened_url_array = []
HEADER_ATTRS_FOR['screened_url'].each do |attr|
data =
if attr == 'action'
action = ScreenedUrl.actions.key(screened_url.attributes['action_type']).to_s
action = "do nothing" if action.blank?
else
screened_url.attributes[attr]
end
screened_url_array.push(data)
end
screened_url_array
end
def notify_user(download_link, file_name, file_size, export_title)
if @current_user
if download_link.present?
SystemMessage.create_from_system_user(
@current_user,
:csv_export_succeeded,
download_link: download_link,
file_name: "#{file_name}.gz",
file_size: file_size,
export_title: export_title
)
else else
SystemMessage.create_from_system_user(@current_user, :csv_export_failed) staff_action.attributes[attr]
end end
staff_action_array.push(data)
end
staff_action_array
end
def get_screened_email_fields(screened_email)
screened_email_array = []
HEADER_ATTRS_FOR['screened_email'].each do |attr|
data =
if attr == 'action'
ScreenedEmail.actions.key(screened_email.attributes['action_type']).to_s
else
screened_email.attributes[attr]
end
screened_email_array.push(data)
end
screened_email_array
end
def get_screened_ip_fields(screened_ip)
screened_ip_array = []
HEADER_ATTRS_FOR['screened_ip'].each do |attr|
data =
if attr == 'action'
ScreenedIpAddress.actions.key(screened_ip.attributes['action_type']).to_s
else
screened_ip.attributes[attr]
end
screened_ip_array.push(data)
end
screened_ip_array
end
def get_screened_url_fields(screened_url)
screened_url_array = []
HEADER_ATTRS_FOR['screened_url'].each do |attr|
data =
if attr == 'action'
action = ScreenedUrl.actions.key(screened_url.attributes['action_type']).to_s
action = "do nothing" if action.blank?
else
screened_url.attributes[attr]
end
screened_url_array.push(data)
end
screened_url_array
end
def notify_user(download_link, file_name, file_size, export_title)
if @current_user
if download_link.present?
SystemMessage.create_from_system_user(
@current_user,
:csv_export_succeeded,
download_link: download_link,
file_name: "#{file_name}.gz",
file_size: file_size,
export_title: export_title
)
else
SystemMessage.create_from_system_user(@current_user, :csv_export_failed)
end end
end end
end
end end
end end

View file

@ -174,9 +174,9 @@ module Jobs
private private
def remove_scheme(src) def remove_scheme(src)
src.sub(/^https?:/i, "") src.sub(/^https?:/i, "")
end end
end end
end end

View file

@ -225,13 +225,13 @@ class Badge < ActiveRecord::Base
protected protected
def ensure_not_system def ensure_not_system
self.id = [Badge.maximum(:id) + 1, 100].max unless id self.id = [Badge.maximum(:id) + 1, 100].max unless id
end end
def i18n_name def i18n_name
self.name.downcase.tr(' ', '_') self.name.downcase.tr(' ', '_')
end end
end end

View file

@ -44,127 +44,127 @@ class CategoryList
private private
def find_relevant_topics def find_relevant_topics
@topics_by_id = {} @topics_by_id = {}
@topics_by_category_id = {} @topics_by_category_id = {}
category_featured_topics = CategoryFeaturedTopic.select([:category_id, :topic_id]).order(:rank) category_featured_topics = CategoryFeaturedTopic.select([:category_id, :topic_id]).order(:rank)
@all_topics = Topic.where(id: category_featured_topics.map(&:topic_id)) @all_topics = Topic.where(id: category_featured_topics.map(&:topic_id))
@all_topics = @all_topics.includes(:last_poster) if @options[:include_topics] @all_topics = @all_topics.includes(:last_poster) if @options[:include_topics]
@all_topics.each do |t| @all_topics.each do |t|
# hint for the serializer # hint for the serializer
t.include_last_poster = true if @options[:include_topics] t.include_last_poster = true if @options[:include_topics]
@topics_by_id[t.id] = t @topics_by_id[t.id] = t
end
category_featured_topics.each do |cft|
@topics_by_category_id[cft.category_id] ||= []
@topics_by_category_id[cft.category_id] << cft.topic_id
end
end end
def find_categories category_featured_topics.each do |cft|
@categories = Category.includes( @topics_by_category_id[cft.category_id] ||= []
:uploaded_background, @topics_by_category_id[cft.category_id] << cft.topic_id
:uploaded_logo, end
:topic_only_relative_url, end
subcategories: [:topic_only_relative_url]
).secured(@guardian)
@categories = @categories.where("categories.parent_category_id = ?", @options[:parent_category_id].to_i) if @options[:parent_category_id].present? def find_categories
@categories = Category.includes(
:uploaded_background,
:uploaded_logo,
:topic_only_relative_url,
subcategories: [:topic_only_relative_url]
).secured(@guardian)
if SiteSetting.fixed_category_positions @categories = @categories.where("categories.parent_category_id = ?", @options[:parent_category_id].to_i) if @options[:parent_category_id].present?
@categories = @categories.order(:position, :id)
else
@categories = @categories.order('COALESCE(categories.posts_week, 0) DESC')
.order('COALESCE(categories.posts_month, 0) DESC')
.order('COALESCE(categories.posts_year, 0) DESC')
.order('id ASC')
end
@categories = @categories.to_a if SiteSetting.fixed_category_positions
@categories = @categories.order(:position, :id)
else
@categories = @categories.order('COALESCE(categories.posts_week, 0) DESC')
.order('COALESCE(categories.posts_month, 0) DESC')
.order('COALESCE(categories.posts_year, 0) DESC')
.order('id ASC')
end
category_user = {} @categories = @categories.to_a
default_notification_level = nil
unless @guardian.anonymous?
category_user = Hash[*CategoryUser.where(user: @guardian.user).pluck(:category_id, :notification_level).flatten]
default_notification_level = CategoryUser.notification_levels[:regular]
end
allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id)) category_user = {}
@categories.each do |category| default_notification_level = nil
category.notification_level = category_user[category.id] || default_notification_level unless @guardian.anonymous?
category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id) category_user = Hash[*CategoryUser.where(user: @guardian.user).pluck(:category_id, :notification_level).flatten]
category.has_children = category.subcategories.present? default_notification_level = CategoryUser.notification_levels[:regular]
end end
if @options[:parent_category_id].blank? allowed_topic_create = Set.new(Category.topic_create_allowed(@guardian).pluck(:id))
subcategories = {} @categories.each do |category|
to_delete = Set.new category.notification_level = category_user[category.id] || default_notification_level
@categories.each do |c| category.permission = CategoryGroup.permission_types[:full] if allowed_topic_create.include?(category.id)
if c.parent_category_id.present? category.has_children = category.subcategories.present?
subcategories[c.parent_category_id] ||= [] end
subcategories[c.parent_category_id] << c.id
to_delete << c if @options[:parent_category_id].blank?
end subcategories = {}
to_delete = Set.new
@categories.each do |c|
if c.parent_category_id.present?
subcategories[c.parent_category_id] ||= []
subcategories[c.parent_category_id] << c.id
to_delete << c
end end
@categories.each { |c| c.subcategory_ids = subcategories[c.id] }
@categories.delete_if { |c| to_delete.include?(c) }
end end
@categories.each { |c| c.subcategory_ids = subcategories[c.id] }
@categories.delete_if { |c| to_delete.include?(c) }
end
if @topics_by_category_id if @topics_by_category_id
@categories.each do |c| @categories.each do |c|
topics_in_cat = @topics_by_category_id[c.id] topics_in_cat = @topics_by_category_id[c.id]
if topics_in_cat.present? if topics_in_cat.present?
c.displayable_topics = [] c.displayable_topics = []
topics_in_cat.each do |topic_id| topics_in_cat.each do |topic_id|
topic = @topics_by_id[topic_id] topic = @topics_by_id[topic_id]
if topic.present? && @guardian.can_see?(topic) if topic.present? && @guardian.can_see?(topic)
# topic.category is very slow under rails 4.2 # topic.category is very slow under rails 4.2
topic.association(:category).target = c topic.association(:category).target = c
c.displayable_topics << topic c.displayable_topics << topic
end
end end
end end
end end
end end
end end
end
def prune_empty def prune_empty
return if SiteSetting.allow_uncategorized_topics return if SiteSetting.allow_uncategorized_topics
@categories.delete_if { |c| c.uncategorized? && c.displayable_topics.blank? } @categories.delete_if { |c| c.uncategorized? && c.displayable_topics.blank? }
end
# Attach some data for serialization to each topic
def find_user_data
if @guardian.current_user && @all_topics.present?
topic_lookup = TopicUser.lookup_for(@guardian.current_user, @all_topics)
@all_topics.each { |ft| ft.user_data = topic_lookup[ft.id] }
end end
end
# Attach some data for serialization to each topic # Put unpinned topics at the end of the list
def find_user_data def sort_unpinned
if @guardian.current_user && @all_topics.present? if @guardian.current_user && @all_topics.present?
topic_lookup = TopicUser.lookup_for(@guardian.current_user, @all_topics) @categories.each do |c|
@all_topics.each { |ft| ft.user_data = topic_lookup[ft.id] } next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
end unpinned = []
end c.displayable_topics.each do |t|
unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data)
# Put unpinned topics at the end of the list end
def sort_unpinned unless unpinned.empty?
if @guardian.current_user && @all_topics.present? c.displayable_topics = (c.displayable_topics - unpinned) + unpinned
@categories.each do |c|
next if c.displayable_topics.blank? || c.displayable_topics.size <= c.num_featured_topics
unpinned = []
c.displayable_topics.each do |t|
unpinned << t if t.pinned_at && PinnedCheck.unpinned?(t, t.user_data)
end
unless unpinned.empty?
c.displayable_topics = (c.displayable_topics - unpinned) + unpinned
end
end end
end end
end end
end
def trim_results def trim_results
@categories.each do |c| @categories.each do |c|
next if c.displayable_topics.blank? next if c.displayable_topics.blank?
c.displayable_topics = c.displayable_topics[0, c.num_featured_topics] c.displayable_topics = c.displayable_topics[0, c.num_featured_topics]
end
end end
end
end end

View file

@ -45,8 +45,8 @@ module Trashable
private private
def trash_update(deleted_at, deleted_by_id) def trash_update(deleted_at, deleted_by_id)
self.update_columns(deleted_at: deleted_at, deleted_by_id: deleted_by_id) self.update_columns(deleted_at: deleted_at, deleted_by_id: deleted_by_id)
end end
end end

View file

@ -53,13 +53,13 @@ class EmbeddableHost < ActiveRecord::Base
private private
def host_must_be_valid def host_must_be_valid
if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,10}(:[0-9]{1,5})?(\/.*)?\Z/i && if host !~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,10}(:[0-9]{1,5})?(\/.*)?\Z/i &&
host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ && host !~ /\A(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(:[0-9]{1,5})?(\/.*)?\Z/ &&
host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i host !~ /\A([a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.)?localhost(\:[0-9]{1,5})?(\/.*)?\Z/i
errors.add(:host, I18n.t('errors.messages.invalid')) errors.add(:host, I18n.t('errors.messages.invalid'))
end
end end
end
end end
# == Schema Information # == Schema Information

View file

@ -598,58 +598,58 @@ class Group < ActiveRecord::Base
protected protected
def name_format_validator def name_format_validator
self.name.strip! self.name.strip!
UsernameValidator.perform_validation(self, 'name') || begin UsernameValidator.perform_validation(self, 'name') || begin
name_lower = self.name.downcase name_lower = self.name.downcase
if self.will_save_change_to_name? && self.name_was&.downcase != name_lower if self.will_save_change_to_name? && self.name_was&.downcase != name_lower
existing = Group.exec_sql( existing = Group.exec_sql(
User::USERNAME_EXISTS_SQL, username: name_lower User::USERNAME_EXISTS_SQL, username: name_lower
).values.present? ).values.present?
if existing if existing
errors.add(:name, I18n.t("activerecord.errors.messages.taken")) errors.add(:name, I18n.t("activerecord.errors.messages.taken"))
end
end end
end end
end end
end
def automatic_membership_email_domains_format_validator def automatic_membership_email_domains_format_validator
return if self.automatic_membership_email_domains.blank? return if self.automatic_membership_email_domains.blank?
domains = self.automatic_membership_email_domains.split("|") domains = self.automatic_membership_email_domains.split("|")
domains.each do |domain| domains.each do |domain|
domain.sub!(/^https?:\/\//, '') domain.sub!(/^https?:\/\//, '')
domain.sub!(/\/.*$/, '') domain.sub!(/\/.*$/, '')
self.errors.add :base, (I18n.t('groups.errors.invalid_domain', domain: domain)) unless domain =~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i self.errors.add :base, (I18n.t('groups.errors.invalid_domain', domain: domain)) unless domain =~ /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,24}(:[0-9]{1,5})?(\/.*)?\Z/i
end
self.automatic_membership_email_domains = domains.join("|")
end end
self.automatic_membership_email_domains = domains.join("|")
end
# hack around AR # hack around AR
def destroy_deletions def destroy_deletions
if @deletions if @deletions
@deletions.each do |gu| @deletions.each do |gu|
gu.destroy gu.destroy
User.where('id = ? AND primary_group_id = ?', gu.user_id, gu.group_id).update_all 'primary_group_id = NULL' User.where('id = ? AND primary_group_id = ?', gu.user_id, gu.group_id).update_all 'primary_group_id = NULL'
end
end
@deletions = nil
end
def automatic_group_membership
if self.automatic_membership_retroactive
Jobs.enqueue(:automatic_group_membership, group_id: self.id)
end end
end end
@deletions = nil
end
def update_title def automatic_group_membership
return if new_record? && !self.title.present? if self.automatic_membership_retroactive
Jobs.enqueue(:automatic_group_membership, group_id: self.id)
end
end
if self.saved_change_to_title? def update_title
sql = <<-SQL.squish return if new_record? && !self.title.present?
if self.saved_change_to_title?
sql = <<-SQL.squish
UPDATE users UPDATE users
SET title = :title SET title = :title
WHERE (title = :title_was OR title = '' OR title IS NULL) WHERE (title = :title_was OR title = '' OR title IS NULL)
@ -657,71 +657,71 @@ class Group < ActiveRecord::Base
AND id IN (SELECT user_id FROM group_users WHERE group_id = :id) AND id IN (SELECT user_id FROM group_users WHERE group_id = :id)
SQL SQL
self.class.exec_sql(sql, title: title, title_was: title_before_last_save, id: id) self.class.exec_sql(sql, title: title, title_was: title_before_last_save, id: id)
end
end end
end
def update_primary_group def update_primary_group
return if new_record? && !self.primary_group? return if new_record? && !self.primary_group?
if self.saved_change_to_primary_group? if self.saved_change_to_primary_group?
sql = <<~SQL sql = <<~SQL
UPDATE users UPDATE users
/*set*/ /*set*/
/*where*/ /*where*/
SQL SQL
builder = SqlBuilder.new(sql) builder = SqlBuilder.new(sql)
builder.where(" builder.where("
id IN ( id IN (
SELECT user_id SELECT user_id
FROM group_users FROM group_users
WHERE group_id = :id WHERE group_id = :id
)", id: id) )", id: id)
if primary_group if primary_group
builder.set("primary_group_id = :id") builder.set("primary_group_id = :id")
else else
builder.set("primary_group_id = NULL") builder.set("primary_group_id = NULL")
builder.where("primary_group_id = :id") builder.where("primary_group_id = :id")
end
builder.exec
end end
builder.exec
end end
end
private private
def validate_grant_trust_level def validate_grant_trust_level
unless TrustLevel.valid?(self.grant_trust_level) unless TrustLevel.valid?(self.grant_trust_level)
self.errors.add(:base, I18n.t( self.errors.add(:base, I18n.t(
'groups.errors.grant_trust_level_not_valid', 'groups.errors.grant_trust_level_not_valid',
trust_level: self.grant_trust_level trust_level: self.grant_trust_level
)) ))
end
end
def can_allow_membership_requests
valid = true
valid =
if self.persisted?
self.group_users.where(owner: true).exists?
else
self.group_users.any?(&:owner)
end end
if !valid
self.errors.add(:base, I18n.t('groups.errors.cant_allow_membership_requests'))
end end
end
def can_allow_membership_requests def enqueue_update_mentions_job
valid = true Jobs.enqueue(:update_group_mentions,
previous_name: self.name_before_last_save,
valid = group_id: self.id
if self.persisted? )
self.group_users.where(owner: true).exists? end
else
self.group_users.any?(&:owner)
end
if !valid
self.errors.add(:base, I18n.t('groups.errors.cant_allow_membership_requests'))
end
end
def enqueue_update_mentions_job
Jobs.enqueue(:update_group_mentions,
previous_name: self.name_before_last_save,
group_id: self.id
)
end
end end
# == Schema Information # == Schema Information

View file

@ -29,11 +29,11 @@ class GroupArchivedMessage < ActiveRecord::Base
private private
def self.publish_topic_tracking_state(topic) def self.publish_topic_tracking_state(topic)
TopicTrackingState.publish_private_message( TopicTrackingState.publish_private_message(
topic, group_archive: true topic, group_archive: true
) )
end end
end end
# == Schema Information # == Schema Information

View file

@ -127,19 +127,19 @@ class PostAnalyzer
private private
def cooked_stripped def cooked_stripped
@cooked_stripped ||= begin @cooked_stripped ||= begin
doc = Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id)) doc = Nokogiri::HTML.fragment(cook(@raw, topic_id: @topic_id))
doc.css("pre .mention, aside.quote > .title, aside.quote .mention, .onebox, .elided").remove doc.css("pre .mention, aside.quote > .title, aside.quote .mention, .onebox, .elided").remove
doc doc
end
end end
end
def link_is_a_mention?(l) def link_is_a_mention?(l)
html_class = l['class'] html_class = l['class']
return false if html_class.blank? return false if html_class.blank?
href = l['href'].to_s href = l['href'].to_s
html_class.to_s['mention'] && href[/^\/u\//] || href[/^\/users\//] html_class.to_s['mention'] && href[/^\/u\//] || href[/^\/users\//]
end end
end end

View file

@ -94,30 +94,30 @@ class QueuedPost < ActiveRecord::Base
private private
def change_to!(state, changed_by) def change_to!(state, changed_by)
state_val = QueuedPost.states[state] state_val = QueuedPost.states[state]
updates = { state: state_val, updates = { state: state_val,
"#{state}_by_id" => changed_by.id, "#{state}_by_id" => changed_by.id,
"#{state}_at" => Time.now } "#{state}_at" => Time.now }
# We use an update with `row_count` trick here to avoid stampeding requests to # We use an update with `row_count` trick here to avoid stampeding requests to
# update the same row simultaneously. Only one state change should go through and # update the same row simultaneously. Only one state change should go through and
# we can use the DB to enforce this # we can use the DB to enforce this
row_count = QueuedPost.where('id = ? AND state <> ?', id, state_val).update_all(updates) row_count = QueuedPost.where('id = ? AND state <> ?', id, state_val).update_all(updates)
raise InvalidStateTransition.new if row_count == 0 raise InvalidStateTransition.new if row_count == 0
if [:rejected, :approved].include?(state) if [:rejected, :approved].include?(state)
UserAction.where(queued_post_id: id).destroy_all UserAction.where(queued_post_id: id).destroy_all
end
# Update the record in memory too, and clear the dirty flag
updates.each { |k, v| send("#{k}=", v) }
changes_applied
QueuedPost.broadcast_new! if visible?
end end
# Update the record in memory too, and clear the dirty flag
updates.each { |k, v| send("#{k}=", v) }
changes_applied
QueuedPost.broadcast_new! if visible?
end
end end
# == Schema Information # == Schema Information

View file

@ -84,8 +84,8 @@ SQL
private private
def update_participant_count def update_participant_count
count = topic.posts.where('NOT hidden AND post_type in (?)', Topic.visible_post_types).count('distinct user_id') count = topic.posts.where('NOT hidden AND post_type in (?)', Topic.visible_post_types).count('distinct user_id')
topic.update_columns(participant_count: count) topic.update_columns(participant_count: count)
end end
end end

View file

@ -83,66 +83,66 @@ class TopicTimer < ActiveRecord::Base
private private
def ensure_update_will_happen def ensure_update_will_happen
if created_at && (execute_at < created_at) if created_at && (execute_at < created_at)
errors.add(:execute_at, I18n.t( errors.add(:execute_at, I18n.t(
'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past' 'activerecord.errors.models.topic_timer.attributes.execute_at.in_the_past'
)) ))
end
end end
end
def cancel_auto_close_job def cancel_auto_close_job
Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id) Jobs.cancel_scheduled_job(:toggle_topic_closed, topic_timer_id: id)
end end
alias_method :cancel_auto_open_job, :cancel_auto_close_job alias_method :cancel_auto_open_job, :cancel_auto_close_job
def cancel_auto_publish_to_category_job def cancel_auto_publish_to_category_job
Jobs.cancel_scheduled_job(:publish_topic_to_category, topic_timer_id: id) Jobs.cancel_scheduled_job(:publish_topic_to_category, topic_timer_id: id)
end end
def cancel_auto_delete_job def cancel_auto_delete_job
Jobs.cancel_scheduled_job(:delete_topic, topic_timer_id: id) Jobs.cancel_scheduled_job(:delete_topic, topic_timer_id: id)
end end
def cancel_auto_reminder_job def cancel_auto_reminder_job
Jobs.cancel_scheduled_job(:topic_reminder, topic_timer_id: id) Jobs.cancel_scheduled_job(:topic_reminder, topic_timer_id: id)
end end
def schedule_auto_open_job(time) def schedule_auto_open_job(time)
return unless topic return unless topic
topic.update_status('closed', true, user) if !topic.closed topic.update_status('closed', true, user) if !topic.closed
Jobs.enqueue_at(time, :toggle_topic_closed, Jobs.enqueue_at(time, :toggle_topic_closed,
topic_timer_id: id, topic_timer_id: id,
state: false state: false
) )
end end
def schedule_auto_close_job(time) def schedule_auto_close_job(time)
return unless topic return unless topic
topic.update_status('closed', false, user) if topic.closed topic.update_status('closed', false, user) if topic.closed
Jobs.enqueue_at(time, :toggle_topic_closed, Jobs.enqueue_at(time, :toggle_topic_closed,
topic_timer_id: id, topic_timer_id: id,
state: true state: true
) )
end end
def schedule_auto_publish_to_category_job(time) def schedule_auto_publish_to_category_job(time)
Jobs.enqueue_at(time, :publish_topic_to_category, topic_timer_id: id) Jobs.enqueue_at(time, :publish_topic_to_category, topic_timer_id: id)
end end
def publishing_to_category? def publishing_to_category?
self.status_type.to_i == TopicTimer.types[:publish_to_category] self.status_type.to_i == TopicTimer.types[:publish_to_category]
end end
def schedule_auto_delete_job(time) def schedule_auto_delete_job(time)
Jobs.enqueue_at(time, :delete_topic, topic_timer_id: id) Jobs.enqueue_at(time, :delete_topic, topic_timer_id: id)
end end
def schedule_auto_reminder_job(time) def schedule_auto_reminder_job(time)
Jobs.enqueue_at(time, :topic_reminder, topic_timer_id: id) Jobs.enqueue_at(time, :topic_reminder, topic_timer_id: id)
end end
end end
# == Schema Information # == Schema Information

View file

@ -19,9 +19,9 @@ class UnsubscribeKey < ActiveRecord::Base
private private
def generate_random_key def generate_random_key
self.key = SecureRandom.hex(32) self.key = SecureRandom.hex(32)
end end
end end
# == Schema Information # == Schema Information

View file

@ -36,11 +36,11 @@ class UserArchivedMessage < ActiveRecord::Base
private private
def self.publish_topic_tracking_state(topic, user_id) def self.publish_topic_tracking_state(topic, user_id)
TopicTrackingState.publish_private_message( TopicTrackingState.publish_private_message(
topic, archive_user_id: user_id topic, archive_user_id: user_id
) )
end end
end end
# == Schema Information # == Schema Information

View file

@ -26,9 +26,9 @@ class UserBadge < ActiveRecord::Base
private private
def single_grant_badge? def single_grant_badge?
self.badge.single_grant? self.badge.single_grant?
end end
end end
# == Schema Information # == Schema Information

View file

@ -141,10 +141,10 @@ class UserOption < ActiveRecord::Base
private private
def update_tracked_topics def update_tracked_topics
return unless saved_change_to_auto_track_topics_after_msecs? return unless saved_change_to_auto_track_topics_after_msecs?
TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call TrackedTopicsUpdater.new(id, auto_track_topics_after_msecs).call
end end
end end

View file

@ -93,9 +93,9 @@ class WebHook < ActiveRecord::Base
private private
def self.guardian def self.guardian
@guardian ||= Guardian.new(Discourse.system_user) @guardian ||= Guardian.new(Discourse.system_user)
end end
end end
# == Schema Information # == Schema Information

View file

@ -81,12 +81,12 @@ class AdminUserActionSerializer < ApplicationSerializer
private private
# we need this to handle deleted topics which aren't loaded via the .includes(:topic) # we need this to handle deleted topics which aren't loaded via the .includes(:topic)
# because Rails 4 "unscoped" support is bugged (cf. https://github.com/rails/rails/issues/13775) # because Rails 4 "unscoped" support is bugged (cf. https://github.com/rails/rails/issues/13775)
def topic def topic
return @topic if @topic return @topic if @topic
@topic = object.topic || Topic.with_deleted.find(object.topic_id) @topic = object.topic || Topic.with_deleted.find(object.topic_id)
@topic @topic
end end
end end

View file

@ -68,15 +68,15 @@ class BasicGroupSerializer < ApplicationSerializer
private private
def staff? def staff?
@staff ||= scope.is_staff? @staff ||= scope.is_staff?
end end
def user_group_ids def user_group_ids
@options[:user_group_ids] @options[:user_group_ids]
end end
def owner_group_ids def owner_group_ids
@options[:owner_group_ids] @options[:owner_group_ids]
end end
end end

View file

@ -121,8 +121,8 @@ class ListableTopicSerializer < BasicTopicSerializer
protected protected
def unread_helper def unread_helper
@unread_helper ||= Unread.new(object, object.user_data, scope) @unread_helper ||= Unread.new(object, object.user_data, scope)
end end
end end

View file

@ -45,9 +45,9 @@ class PostActionTypeSerializer < ApplicationSerializer
protected protected
def i18n(field, vars = nil) def i18n(field, vars = nil)
key = "post_action_types.#{name_key}.#{field}" key = "post_action_types.#{name_key}.#{field}"
vars ? I18n.t(key, vars) : I18n.t(key) vars ? I18n.t(key, vars) : I18n.t(key)
end end
end end

View file

@ -173,94 +173,94 @@ class PostRevisionSerializer < ApplicationSerializer
protected protected
def post def post
@post ||= object.post @post ||= object.post
end
def topic
@topic ||= object.post.topic
end
def revisions
@revisions ||= all_revisions.select { |r| scope.can_view_hidden_post_revisions? || !r["hidden"] }
end
def all_revisions
return @all_revisions if @all_revisions
post_revisions = PostRevision.where(post_id: object.post_id).order(:number).to_a
latest_modifications = {
"raw" => [post.raw],
"cooked" => [post.cooked],
"edit_reason" => [post.edit_reason],
"wiki" => [post.wiki],
"post_type" => [post.post_type],
"user_id" => [post.user_id]
}
# Retrieve any `tracked_topic_fields`
PostRevisor.tracked_topic_fields.each_key do |field|
latest_modifications[field.to_s] = [topic.send(field)] if topic.respond_to?(field)
end end
def topic latest_modifications["featured_link"] = [post.topic.featured_link] if SiteSetting.topic_featured_link_enabled
@topic ||= object.post.topic latest_modifications["tags"] = [topic.tags.pluck(:name)] if scope.can_see_tags?(topic)
end
def revisions post_revisions << PostRevision.new(
@revisions ||= all_revisions.select { |r| scope.can_view_hidden_post_revisions? || !r["hidden"] } number: post_revisions.last.number + 1,
end hidden: post.hidden,
modifications: latest_modifications
)
def all_revisions @all_revisions = []
return @all_revisions if @all_revisions
post_revisions = PostRevision.where(post_id: object.post_id).order(:number).to_a # backtrack
post_revisions.each do |pr|
revision = HashWithIndifferentAccess.new
revision[:revision] = pr.number
revision[:hidden] = pr.hidden
latest_modifications = { pr.modifications.each_key do |field|
"raw" => [post.raw], revision[field] = pr.modifications[field][0]
"cooked" => [post.cooked],
"edit_reason" => [post.edit_reason],
"wiki" => [post.wiki],
"post_type" => [post.post_type],
"user_id" => [post.user_id]
}
# Retrieve any `tracked_topic_fields`
PostRevisor.tracked_topic_fields.each_key do |field|
latest_modifications[field.to_s] = [topic.send(field)] if topic.respond_to?(field)
end end
latest_modifications["featured_link"] = [post.topic.featured_link] if SiteSetting.topic_featured_link_enabled @all_revisions << revision
latest_modifications["tags"] = [topic.tags.pluck(:name)] if scope.can_see_tags?(topic)
post_revisions << PostRevision.new(
number: post_revisions.last.number + 1,
hidden: post.hidden,
modifications: latest_modifications
)
@all_revisions = []
# backtrack
post_revisions.each do |pr|
revision = HashWithIndifferentAccess.new
revision[:revision] = pr.number
revision[:hidden] = pr.hidden
pr.modifications.each_key do |field|
revision[field] = pr.modifications[field][0]
end
@all_revisions << revision
end
# waterfall
(@all_revisions.count - 1).downto(1).each do |r|
cur = @all_revisions[r]
prev = @all_revisions[r - 1]
cur.each_key do |field|
prev[field] = prev.has_key?(field) ? prev[field] : cur[field]
end
end
@all_revisions
end end
def previous # waterfall
@previous ||= revisions.select { |r| r["revision"] <= current_revision }.last (@all_revisions.count - 1).downto(1).each do |r|
end cur = @all_revisions[r]
prev = @all_revisions[r - 1]
def current cur.each_key do |field|
@current ||= revisions.select { |r| r["revision"] > current_revision }.first prev[field] = prev.has_key?(field) ? prev[field] : cur[field]
end
def user
# if stuff goes pear shape attribute to system
object.user || Discourse.system_user
end
def filter_visible_tags(tags)
if tags.is_a?(Array) && tags.size > 0
@hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope)
tags - @hidden_tag_names
else
tags
end end
end end
@all_revisions
end
def previous
@previous ||= revisions.select { |r| r["revision"] <= current_revision }.last
end
def current
@current ||= revisions.select { |r| r["revision"] > current_revision }.first
end
def user
# if stuff goes pear shape attribute to system
object.user || Discourse.system_user
end
def filter_visible_tags(tags)
if tags.is_a?(Array) && tags.size > 0
@hidden_tag_names ||= DiscourseTagging.hidden_tag_names(scope)
tags - @hidden_tag_names
else
tags
end
end
end end

View file

@ -380,26 +380,26 @@ class PostSerializer < BasicPostSerializer
private private
def topic def topic
@topic = object.topic @topic = object.topic
@topic ||= Topic.with_deleted.find(object.topic_id) if scope.is_staff? @topic ||= Topic.with_deleted.find(object.topic_id) if scope.is_staff?
@topic @topic
end end
def post_actions def post_actions
@post_actions ||= (@topic_view&.all_post_actions || {})[object.id] @post_actions ||= (@topic_view&.all_post_actions || {})[object.id]
end end
def active_flags def active_flags
@active_flags ||= (@topic_view&.all_active_flags || {})[object.id] @active_flags ||= (@topic_view&.all_active_flags || {})[object.id]
end end
def post_custom_fields def post_custom_fields
@post_custom_fields ||= if @topic_view @post_custom_fields ||= if @topic_view
(@topic_view.post_custom_fields || {})[object.id] || {} (@topic_view.post_custom_fields || {})[object.id] || {}
else else
object.custom_fields object.custom_fields
end
end end
end
end end

View file

@ -2,9 +2,9 @@ class TopicFlagTypeSerializer < PostActionTypeSerializer
protected protected
def i18n(field, vars = nil) def i18n(field, vars = nil)
key = "topic_flag_types.#{name_key}.#{field}" key = "topic_flag_types.#{name_key}.#{field}"
vars ? I18n.t(key, vars) : I18n.t(key) vars ? I18n.t(key, vars) : I18n.t(key)
end end
end end

View file

@ -301,8 +301,8 @@ class TopicViewSerializer < ApplicationSerializer
private private
def private_message?(topic) def private_message?(topic)
@private_message ||= topic.private_message? @private_message ||= topic.private_message?
end end
end end

View file

@ -58,21 +58,21 @@ class GroupActionLogger
private private
def excluded_attributes def excluded_attributes
[ [
:bio_cooked, :bio_cooked,
:updated_at, :updated_at,
:created_at, :created_at,
:user_count :user_count
] ]
end end
def default_params def default_params
{ group: @group, acting_user: @acting_user } { group: @group, acting_user: @acting_user }
end end
def can_edit? def can_edit?
raise Discourse::InvalidParameters.new unless Guardian.new(@acting_user).can_log_group_changes?(@group) raise Discourse::InvalidParameters.new unless Guardian.new(@acting_user).can_log_group_changes?(@group)
end end
end end

View file

@ -550,13 +550,13 @@ class StaffActionLogger
private private
def params(opts = nil) def params(opts = nil)
opts ||= {} opts ||= {}
{ acting_user_id: @admin.id, context: opts[:context] } { acting_user_id: @admin.id, context: opts[:context] }
end end
def validate_category(category) def validate_category(category)
raise Discourse::InvalidParameters.new(:category) unless category && category.is_a?(Category) raise Discourse::InvalidParameters.new(:category) unless category && category.is_a?(Category)
end end
end end

View file

@ -77,13 +77,13 @@ class UserAnonymizer
private private
def make_anon_username def make_anon_username
100.times do 100.times do
new_username = "anon#{(SecureRandom.random_number * 100000000).to_i}" new_username = "anon#{(SecureRandom.random_number * 100000000).to_i}"
return new_username unless User.where(username_lower: new_username).exists? return new_username unless User.where(username_lower: new_username).exists?
end
raise "Failed to generate an anon username"
end end
raise "Failed to generate an anon username"
end
def ip_where(column = 'user_id') def ip_where(column = 'user_id')
["#{column} = :user_id AND ip_address IS NOT NULL", user_id: @user.id] ["#{column} = :user_id AND ip_address IS NOT NULL", user_id: @user.id]

View file

@ -63,58 +63,58 @@ class Auth::FacebookAuthenticator < Auth::Authenticator
protected protected
def parse_auth_token(auth_token) def parse_auth_token(auth_token)
raw_info = auth_token["extra"]["raw_info"] raw_info = auth_token["extra"]["raw_info"]
info = auth_token["info"] info = auth_token["info"]
email = auth_token["info"][:email] email = auth_token["info"][:email]
website = (info["urls"] && info["urls"]["Website"]) || nil website = (info["urls"] && info["urls"]["Website"]) || nil
{ {
facebook: { facebook: {
facebook_user_id: auth_token["uid"], facebook_user_id: auth_token["uid"],
link: raw_info["link"], link: raw_info["link"],
username: raw_info["username"], username: raw_info["username"],
first_name: raw_info["first_name"], first_name: raw_info["first_name"],
last_name: raw_info["last_name"], last_name: raw_info["last_name"],
email: email,
gender: raw_info["gender"],
name: raw_info["name"],
avatar_url: info["image"],
location: info["location"],
website: website,
about_me: info["description"]
},
email: email, email: email,
email_valid: true gender: raw_info["gender"],
} name: raw_info["name"],
avatar_url: info["image"],
location: info["location"],
website: website,
about_me: info["description"]
},
email: email,
email_valid: true
}
end
def retrieve_avatar(user, data)
return unless user
return if user.user_avatar.try(:custom_upload_id).present?
if (avatar_url = data[:avatar_url]).present?
url = "#{avatar_url}?height=#{AVATAR_SIZE}&width=#{AVATAR_SIZE}"
Jobs.enqueue(:download_avatar_from_url, url: url, user_id: user.id, override_gravatar: false)
end end
end
def retrieve_avatar(user, data) def retrieve_profile(user, data)
return unless user return unless user
return if user.user_avatar.try(:custom_upload_id).present?
if (avatar_url = data[:avatar_url]).present? bio = data[:about_me] || data[:about]
url = "#{avatar_url}?height=#{AVATAR_SIZE}&width=#{AVATAR_SIZE}" location = data[:location]
Jobs.enqueue(:download_avatar_from_url, url: url, user_id: user.id, override_gravatar: false) website = data[:website]
end
end if bio || location || website
profile = user.user_profile
def retrieve_profile(user, data) profile.bio_raw = bio unless profile.bio_raw.present?
return unless user profile.location = location unless profile.location.present?
profile.website = website unless profile.website.present?
bio = data[:about_me] || data[:about] profile.save
location = data[:location]
website = data[:website]
if bio || location || website
profile = user.user_profile
profile.bio_raw = bio unless profile.bio_raw.present?
profile.location = location unless profile.location.present?
profile.website = website unless profile.website.present?
profile.save
end
end end
end
end end

View file

@ -69,28 +69,28 @@ class Auth::TwitterAuthenticator < Auth::Authenticator
protected protected
def retrieve_avatar(user, data) def retrieve_avatar(user, data)
return unless user return unless user
return if user.user_avatar.try(:custom_upload_id).present? return if user.user_avatar.try(:custom_upload_id).present?
if (avatar_url = data[:twitter_image]).present? if (avatar_url = data[:twitter_image]).present?
url = avatar_url.sub("_normal", "") url = avatar_url.sub("_normal", "")
Jobs.enqueue(:download_avatar_from_url, url: url, user_id: user.id, override_gravatar: false) Jobs.enqueue(:download_avatar_from_url, url: url, user_id: user.id, override_gravatar: false)
end
end end
end
def retrieve_profile(user, data) def retrieve_profile(user, data)
return unless user return unless user
bio = data[:twitter_description] bio = data[:twitter_description]
location = data[:twitter_location] location = data[:twitter_location]
if bio || location if bio || location
profile = user.user_profile 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.location = location unless profile.location.present?
profile.save profile.save
end
end end
end
end end

View file

@ -23,32 +23,32 @@ class CommonPasswords
private private
class RedisPasswordList class RedisPasswordList
def include?(password) def include?(password)
CommonPasswords.redis.sismember CommonPasswords::LIST_KEY, password CommonPasswords.redis.sismember CommonPasswords::LIST_KEY, password
end
end end
end
def self.password_list def self.password_list
@mutex.synchronize do @mutex.synchronize do
load_passwords unless redis.scard(LIST_KEY) > 0 load_passwords unless redis.scard(LIST_KEY) > 0
end
RedisPasswordList.new
end end
RedisPasswordList.new
end
def self.redis def self.redis
$redis.without_namespace $redis.without_namespace
end end
def self.load_passwords def self.load_passwords
passwords = File.readlines(PASSWORD_FILE) passwords = File.readlines(PASSWORD_FILE)
passwords.map!(&:chomp).each do |pwd| passwords.map!(&:chomp).each do |pwd|
# slower, but a tad more compatible # slower, but a tad more compatible
redis.sadd LIST_KEY, pwd redis.sadd LIST_KEY, pwd
end
rescue Errno::ENOENT
# 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
rescue Errno::ENOENT
# 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 end

View file

@ -193,20 +193,20 @@ class ComposerMessagesFinder
private private
def educate_reply?(type) def educate_reply?(type)
replying? && replying? &&
@details[:topic_id] && @details[:topic_id] &&
(@topic.present? && !@topic.private_message?) && (@topic.present? && !@topic.private_message?) &&
(@user.post_count >= SiteSetting.educate_until_posts) && (@user.post_count >= SiteSetting.educate_until_posts) &&
!UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id]) !UserHistory.exists_for_user?(@user, type, topic_id: @details[:topic_id])
end end
def creating_topic? def creating_topic?
@details[:composer_action] == "createTopic" @details[:composer_action] == "createTopic"
end end
def replying? def replying?
@details[:composer_action] == "reply" @details[:composer_action] == "reply"
end end
end end

View file

@ -104,19 +104,19 @@ class EmailUpdater
protected protected
def notify_old(old_email, new_email) def notify_old(old_email, new_email)
Jobs.enqueue :critical_user_email, Jobs.enqueue :critical_user_email,
to_address: old_email, to_address: old_email,
type: :notify_old_email, type: :notify_old_email,
user_id: @user.id user_id: @user.id
end end
def send_email(type, email_token) def send_email(type, email_token)
Jobs.enqueue :critical_user_email, Jobs.enqueue :critical_user_email,
to_address: email_token.email, to_address: email_token.email,
type: type, type: type,
user_id: @user.id, user_id: @user.id,
email_token: email_token.token email_token: email_token.token
end end
end end

View file

@ -96,12 +96,12 @@ class FileHelper
private private
def self.images def self.images
@@images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico} @@images ||= Set.new %w{jpg jpeg png gif tif tiff bmp svg webp ico}
end end
def self.images_regexp def self.images_regexp
@@images_regexp ||= /\.(#{images.to_a.join("|")})$/i @@images_regexp ||= /\.(#{images.to_a.join("|")})$/i
end end
end end

View file

@ -224,14 +224,14 @@ module FlagQuery
private private
def self.excerpt(cooked) def self.excerpt(cooked)
excerpt = Post.excerpt(cooked, 200, keep_emoji_images: true) excerpt = Post.excerpt(cooked, 200, keep_emoji_images: true)
# remove the first link if it's the first node # remove the first link if it's the first node
fragment = Nokogiri::HTML.fragment(excerpt) fragment = Nokogiri::HTML.fragment(excerpt)
if fragment.children.first == fragment.css("a:first").first && fragment.children.first if fragment.children.first == fragment.css("a:first").first && fragment.children.first
fragment.children.first.remove fragment.children.first.remove
end
fragment.to_html.strip
end end
fragment.to_html.strip
end
end end

View file

@ -3,40 +3,40 @@
class Rack::ETag class Rack::ETag
private private
def digest_body(body) def digest_body(body)
parts = [] parts = []
has_body = false has_body = false
body.each do |part| body.each do |part|
parts << part parts << part
has_body ||= part.length > 0 has_body ||= part.length > 0
end end
hexdigest = hexdigest =
if has_body if has_body
digest = Digest::MD5.new digest = Digest::MD5.new
parts.each { |part| digest << part } parts.each { |part| digest << part }
digest.hexdigest digest.hexdigest
end end
[hexdigest, parts] [hexdigest, parts]
end end
end end
# patch https://github.com/rack/rack/pull/596 # patch https://github.com/rack/rack/pull/596
# #
class Rack::ConditionalGet class Rack::ConditionalGet
private private
def to_rfc2822(since) def to_rfc2822(since)
# shortest possible valid date is the obsolete: 1 Nov 97 09:55 A # shortest possible valid date is the obsolete: 1 Nov 97 09:55 A
# anything shorter is invalid, this avoids exceptions for common cases # anything shorter is invalid, this avoids exceptions for common cases
# most common being the empty string # most common being the empty string
if since && since.length >= 16 if since && since.length >= 16
# NOTE: there is no trivial way to write this in a non execption way # NOTE: there is no trivial way to write this in a non execption way
# _rfc2822 returns a hash but is not that usable # _rfc2822 returns a hash but is not that usable
Time.rfc2822(since) rescue nil Time.rfc2822(since) rescue nil
else else
nil nil
end end
end end
end end

View file

@ -52,54 +52,54 @@ module I18n
protected protected
def find_results(regexp, results, translations, path = nil) def find_results(regexp, results, translations, path = nil)
return results if translations.blank? return results if translations.blank?
translations.each do |k_sym, v| translations.each do |k_sym, v|
k = k_sym.to_s k = k_sym.to_s
key_path = path ? "#{path}.#{k}" : k key_path = path ? "#{path}.#{k}" : k
if v.is_a?(String) if v.is_a?(String)
unless results.has_key?(key_path) unless results.has_key?(key_path)
results[key_path] = v if key_path =~ regexp || v =~ regexp results[key_path] = v if key_path =~ regexp || v =~ regexp
end
elsif v.is_a?(Hash)
find_results(regexp, results, v, key_path)
end end
elsif v.is_a?(Hash)
find_results(regexp, results, v, key_path)
end end
results
end end
results
end
# Support interpolation and pluralization of overrides by first looking up # Support interpolation and pluralization of overrides by first looking up
# the original translations before applying our overrides. # the original translations before applying our overrides.
def lookup(locale, key, scope = [], options = {}) def lookup(locale, key, scope = [], options = {})
existing_translations = super(locale, key, scope, options) existing_translations = super(locale, key, scope, options)
return existing_translations if scope.is_a?(Array) && scope.include?(:models) return existing_translations if scope.is_a?(Array) && scope.include?(:models)
overrides = options.dig(:overrides, locale) overrides = options.dig(:overrides, locale)
if overrides if overrides
if existing_translations && options[:count] if existing_translations && options[:count]
remapped_translations = remapped_translations =
if existing_translations.is_a?(Hash) if existing_translations.is_a?(Hash)
Hash[existing_translations.map { |k, v| ["#{key}.#{k}", v] }] Hash[existing_translations.map { |k, v| ["#{key}.#{k}", v] }]
elsif existing_translations.is_a?(String) elsif existing_translations.is_a?(String)
Hash[[[key, existing_translations]]] Hash[[[key, existing_translations]]]
end
result = {}
remapped_translations.merge(overrides).each do |k, v|
result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key.to_s)
end end
return result if result.size > 0
end
return overrides[key] if overrides[key] result = {}
remapped_translations.merge(overrides).each do |k, v|
result[k.split('.').last.to_sym] = v if k != key && k.start_with?(key.to_s)
end
return result if result.size > 0
end end
existing_translations return overrides[key] if overrides[key]
end end
existing_translations
end
end end
end end
end end

View file

@ -10,8 +10,8 @@ class DuplicateKeyFinder < LocaleFileWalker
protected protected
def handle_scalar(node, depth, parents) def handle_scalar(node, depth, parents)
super super
@keys_with_count[parents.join('.')] += 1 @keys_with_count[parents.join('.')] += 1
end end
end end

View file

@ -63,20 +63,20 @@ class InlineOneboxer
private private
def self.onebox_for(url, title, opts) def self.onebox_for(url, title, opts)
onebox = { onebox = {
url: url, url: url,
title: title && Emoji.gsub_emoji_to_unicode(title) title: title && Emoji.gsub_emoji_to_unicode(title)
} }
unless opts[:skip_cache] unless opts[:skip_cache]
Rails.cache.write(cache_key(url), onebox, expires_in: 1.day) Rails.cache.write(cache_key(url), onebox, expires_in: 1.day)
end
onebox
end end
def self.cache_key(url) onebox
"inline_onebox:#{url}" end
end
def self.cache_key(url)
"inline_onebox:#{url}"
end
end end

View file

@ -16,22 +16,22 @@ module Onebox
private private
# overwrite to whitelist iframes # overwrite to whitelist iframes
def is_embedded? def is_embedded?
return false unless data[:html] && data[:height] return false unless data[:html] && data[:height]
return true if WhitelistedGenericOnebox.html_providers.include?(data[:provider_name]) return true if WhitelistedGenericOnebox.html_providers.include?(data[:provider_name])
if data[:html]["iframe"] if data[:html]["iframe"]
fragment = Nokogiri::HTML::fragment(data[:html]) fragment = Nokogiri::HTML::fragment(data[:html])
if iframe = fragment.at_css("iframe") if iframe = fragment.at_css("iframe")
src = iframe["src"] src = iframe["src"]
return src.present? && SiteSetting.allowed_iframes.split("|").any? { |url| src.start_with?(url) } return src.present? && SiteSetting.allowed_iframes.split("|").any? { |url| src.start_with?(url) }
end
end end
false
end end
false
end
end end
end end
end end

View file

@ -125,151 +125,151 @@ module Oneboxer
private private
def self.preview_key(user_id) def self.preview_key(user_id)
"onebox:preview:#{user_id}" "onebox:preview:#{user_id}"
end end
def self.blank_onebox def self.blank_onebox
{ preview: "", onebox: "" } { preview: "", onebox: "" }
end end
def self.onebox_cache_key(url) def self.onebox_cache_key(url)
"onebox__#{url}" "onebox__#{url}"
end end
def self.onebox_raw(url, opts = {}) def self.onebox_raw(url, opts = {})
url = URI(url).to_s url = URI(url).to_s
local_onebox(url, opts) || external_onebox(url) local_onebox(url, opts) || external_onebox(url)
rescue => e rescue => e
# no point warning here, just cause we have an issue oneboxing a url # no point warning here, just cause we have an issue oneboxing a url
# we can later hunt for failed oneboxes by searching logs if needed # we can later hunt for failed oneboxes by searching logs if needed
Rails.logger.info("Failed to onebox #{url} #{e} #{e.backtrace}") Rails.logger.info("Failed to onebox #{url} #{e} #{e.backtrace}")
# return a blank hash, so rest of the code works # return a blank hash, so rest of the code works
blank_onebox blank_onebox
end end
def self.local_onebox(url, opts = {}) def self.local_onebox(url, opts = {})
return unless route = Discourse.route_for(url) return unless route = Discourse.route_for(url)
html = html =
case route[:controller] case route[:controller]
when "uploads" then local_upload_html(url) when "uploads" then local_upload_html(url)
when "topics" then local_topic_html(url, route, opts) when "topics" then local_topic_html(url, route, opts)
when "users" then local_user_html(url, route) when "users" then local_user_html(url, route)
end
html = html.presence || "<a href='#{url}'>#{url}</a>"
{ onebox: html, preview: html }
end
def self.local_upload_html(url)
case File.extname(URI(url).path || "")
when /^\.(mov|mp4|webm|ogv)$/i
"<video width='100%' height='100%' controls><source src='#{url}'><a href='#{url}'>#{url}</a></video>"
when /^\.(mp3|ogg|wav|m4a)$/i
"<audio controls><source src='#{url}'><a href='#{url}'>#{url}</a></audio>"
end
end
def self.local_topic_html(url, route, opts)
return unless current_user = User.find_by(id: opts[:user_id])
if current_category = Category.find_by(id: opts[:category_id])
return unless Guardian.new(current_user).can_see_category?(current_category)
end end
if current_topic = Topic.find_by(id: opts[:topic_id]) html = html.presence || "<a href='#{url}'>#{url}</a>"
return unless Guardian.new(current_user).can_see_topic?(current_topic) { onebox: html, preview: html }
end end
topic = Topic.find_by(id: route[:topic_id]) def self.local_upload_html(url)
case File.extname(URI(url).path || "")
when /^\.(mov|mp4|webm|ogv)$/i
"<video width='100%' height='100%' controls><source src='#{url}'><a href='#{url}'>#{url}</a></video>"
when /^\.(mp3|ogg|wav|m4a)$/i
"<audio controls><source src='#{url}'><a href='#{url}'>#{url}</a></audio>"
end
end
return unless topic def self.local_topic_html(url, route, opts)
return if topic.private_message? return unless current_user = User.find_by(id: opts[:user_id])
if current_category&.id != topic.category_id if current_category = Category.find_by(id: opts[:category_id])
return unless Guardian.new.can_see_topic?(topic) return unless Guardian.new(current_user).can_see_category?(current_category)
end
post_number = route[:post_number].to_i
post = post_number > 1 ?
topic.posts.where(post_number: post_number).first :
topic.ordered_posts.first
return if !post || post.hidden || post.post_type != Post.types[:regular]
if post_number > 1 && current_topic&.id == topic.id
excerpt = post.excerpt(SiteSetting.post_onebox_maxlength)
excerpt.gsub!(/[\r\n]+/, " ")
excerpt.gsub!("[/quote]", "[quote]") # don't break my quote
quote = "[quote=\"#{post.user.username}, topic:#{topic.id}, post:#{post.post_number}\"]\n#{excerpt}\n[/quote]"
PrettyText.cook(quote)
else
args = {
topic_id: topic.id,
post_number: post.post_number,
avatar: PrettyText.avatar_img(post.user.avatar_template, "tiny"),
original_url: url,
title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)),
category_html: CategoryBadge.html_for(topic.category),
quote: PrettyText.unescape_emoji(post.excerpt(SiteSetting.post_onebox_maxlength)),
}
template = File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs")
Mustache.render(template, args)
end
end end
def self.local_user_html(url, route) if current_topic = Topic.find_by(id: opts[:topic_id])
username = route[:username] || "" return unless Guardian.new(current_user).can_see_topic?(current_topic)
if user = User.find_by(username_lower: username.downcase)
args = {
user_id: user.id,
username: user.username,
avatar: PrettyText.avatar_img(user.avatar_template, "extra_large"),
name: user.name,
bio: user.user_profile.bio_excerpt(230),
location: user.user_profile.location,
joined: I18n.t('joined'),
created_at: user.created_at.strftime(I18n.t('datetime_formats.formats.date_only')),
website: user.user_profile.website,
website_name: UserSerializer.new(user).website_name,
original_url: url
}
template = File.read("#{Rails.root}/lib/onebox/templates/discourse_user_onebox.hbs")
Mustache.render(template, args)
else
nil
end
end end
def self.external_onebox(url) topic = Topic.find_by(id: route[:topic_id])
Rails.cache.fetch(onebox_cache_key(url), expires_in: 1.day) do
fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, force_get_hosts: force_get_hosts)
uri = fd.resolve
return blank_onebox if uri.blank? || SiteSetting.onebox_domains_blacklist.include?(uri.hostname)
options = { return unless topic
cache: {}, return if topic.private_message?
max_width: 695,
sanitize_config: Sanitize::Config::DISCOURSE_ONEBOX
}
options[:cookie] = fd.cookie if fd.cookie if current_category&.id != topic.category_id
return unless Guardian.new.can_see_topic?(topic)
if Rails.env.development? && SiteSetting.port.to_i > 0
Onebox.options = { allowed_ports: [80, 443, SiteSetting.port.to_i] }
end
r = Onebox.preview(uri.to_s, options)
{ onebox: r.to_s, preview: r&.placeholder_html.to_s }
end
end end
post_number = route[:post_number].to_i
post = post_number > 1 ?
topic.posts.where(post_number: post_number).first :
topic.ordered_posts.first
return if !post || post.hidden || post.post_type != Post.types[:regular]
if post_number > 1 && current_topic&.id == topic.id
excerpt = post.excerpt(SiteSetting.post_onebox_maxlength)
excerpt.gsub!(/[\r\n]+/, " ")
excerpt.gsub!("[/quote]", "[quote]") # don't break my quote
quote = "[quote=\"#{post.user.username}, topic:#{topic.id}, post:#{post.post_number}\"]\n#{excerpt}\n[/quote]"
PrettyText.cook(quote)
else
args = {
topic_id: topic.id,
post_number: post.post_number,
avatar: PrettyText.avatar_img(post.user.avatar_template, "tiny"),
original_url: url,
title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)),
category_html: CategoryBadge.html_for(topic.category),
quote: PrettyText.unescape_emoji(post.excerpt(SiteSetting.post_onebox_maxlength)),
}
template = File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs")
Mustache.render(template, args)
end
end
def self.local_user_html(url, route)
username = route[:username] || ""
if user = User.find_by(username_lower: username.downcase)
args = {
user_id: user.id,
username: user.username,
avatar: PrettyText.avatar_img(user.avatar_template, "extra_large"),
name: user.name,
bio: user.user_profile.bio_excerpt(230),
location: user.user_profile.location,
joined: I18n.t('joined'),
created_at: user.created_at.strftime(I18n.t('datetime_formats.formats.date_only')),
website: user.user_profile.website,
website_name: UserSerializer.new(user).website_name,
original_url: url
}
template = File.read("#{Rails.root}/lib/onebox/templates/discourse_user_onebox.hbs")
Mustache.render(template, args)
else
nil
end
end
def self.external_onebox(url)
Rails.cache.fetch(onebox_cache_key(url), expires_in: 1.day) do
fd = FinalDestination.new(url, ignore_redirects: ignore_redirects, force_get_hosts: force_get_hosts)
uri = fd.resolve
return blank_onebox if uri.blank? || SiteSetting.onebox_domains_blacklist.include?(uri.hostname)
options = {
cache: {},
max_width: 695,
sanitize_config: Sanitize::Config::DISCOURSE_ONEBOX
}
options[:cookie] = fd.cookie if fd.cookie
if Rails.env.development? && SiteSetting.port.to_i > 0
Onebox.options = { allowed_ports: [80, 443, SiteSetting.port.to_i] }
end
r = Onebox.preview(uri.to_s, options)
{ onebox: r.to_s, preview: r&.placeholder_html.to_s }
end
end
end end

View file

@ -37,36 +37,36 @@ module RetrieveTitle
private private
def self.max_chunk_size(uri) def self.max_chunk_size(uri)
# Amazon and YouTube leave the title until very late. Exceptions are bad # Amazon and YouTube leave the title until very late. Exceptions are bad
# but these are large sites. # but these are large sites.
return 500 if uri.host =~ /amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/ return 500 if uri.host =~ /amazon\.(com|ca|co\.uk|es|fr|de|it|com\.au|com\.br|cn|in|co\.jp|com\.mx)$/
return 300 if uri.host =~ /youtube\.com$/ || uri.host =~ /youtu.be/ return 300 if uri.host =~ /youtube\.com$/ || uri.host =~ /youtu.be/
# default is 10k # default is 10k
10 10
end end
# Fetch the beginning of a HTML document at a url # Fetch the beginning of a HTML document at a url
def self.fetch_title(url) def self.fetch_title(url)
fd = FinalDestination.new(url, timeout: CRAWL_TIMEOUT) fd = FinalDestination.new(url, timeout: CRAWL_TIMEOUT)
current = nil current = nil
title = nil title = nil
fd.get do |_response, chunk, uri| fd.get do |_response, chunk, uri|
if current if current
current << chunk current << chunk
else else
current = chunk current = chunk
end
max_size = max_chunk_size(uri) * 1024
title = extract_title(current)
throw :done if title || max_size < current.length
end end
return title
max_size = max_chunk_size(uri) * 1024
title = extract_title(current)
throw :done if title || max_size < current.length
end end
return title
end
end end

View file

@ -503,399 +503,399 @@ class Search
private private
def process_advanced_search!(term) def process_advanced_search!(term)
term.to_s.scan(/(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/).to_a.map do |(word, _)| term.to_s.scan(/(([^" \t\n\x0B\f\r]+)?(("[^"]+")?))/).to_a.map do |(word, _)|
next if word.blank? next if word.blank?
found = false found = false
Search.advanced_filters.each do |matcher, block| Search.advanced_filters.each do |matcher, block|
cleaned = word.gsub(/["']/, "") cleaned = word.gsub(/["']/, "")
if cleaned =~ matcher if cleaned =~ matcher
(@filters ||= []) << [block, $1] (@filters ||= []) << [block, $1]
found = true found = true
end
end
@in_title = false
if word == 'order:latest' || word == 'l'
@order = :latest
nil
elsif word == 'order:latest_topic'
@order = :latest_topic
nil
elsif word == 'in:title'
@in_title = true
nil
elsif word =~ /topic:(\d+)/
topic_id = $1.to_i
if topic_id > 1
topic = Topic.find_by(id: topic_id)
if @guardian.can_see?(topic)
@search_context = topic
end end
end end
nil
@in_title = false elsif word == 'order:views'
@order = :views
if word == 'order:latest' || word == 'l' nil
@order = :latest elsif word == 'order:likes'
nil @order = :likes
elsif word == 'order:latest_topic' nil
@order = :latest_topic elsif word == 'in:private'
nil @search_pms = true
elsif word == 'in:title' nil
@in_title = true elsif word =~ /^private_messages:(.+)$/
nil @search_pms = true
elsif word =~ /topic:(\d+)/ nil
topic_id = $1.to_i
if topic_id > 1
topic = Topic.find_by(id: topic_id)
if @guardian.can_see?(topic)
@search_context = topic
end
end
nil
elsif word == 'order:views'
@order = :views
nil
elsif word == 'order:likes'
@order = :likes
nil
elsif word == 'in:private'
@search_pms = true
nil
elsif word =~ /^private_messages:(.+)$/
@search_pms = true
nil
else
found ? nil : word
end
end.compact.join(' ')
end
def find_grouped_results
if @results.type_filter.present?
raise Discourse::InvalidAccess.new("invalid type filter") unless Search.facets.include?(@results.type_filter)
send("#{@results.type_filter}_search")
else else
unless @search_context found ? nil : word
user_search if @term.present? end
category_search if @term.present? end.compact.join(' ')
tags_search if @term.present? end
def find_grouped_results
if @results.type_filter.present?
raise Discourse::InvalidAccess.new("invalid type filter") unless Search.facets.include?(@results.type_filter)
send("#{@results.type_filter}_search")
else
unless @search_context
user_search if @term.present?
category_search if @term.present?
tags_search if @term.present?
end
topic_search
end
add_more_topics_if_expected
@results
rescue ActiveRecord::StatementInvalid
# In the event of a PG:Error return nothing, it is likely they used a foreign language whose
# locale is not supported by postgres
end
# Add more topics if we expected them
def add_more_topics_if_expected
expected_topics = 0
expected_topics = Search.facets.size unless @results.type_filter.present?
expected_topics = Search.per_facet * Search.facets.size if @results.type_filter == 'topic'
expected_topics -= @results.posts.length
if expected_topics > 0
extra_posts = posts_query(expected_topics * Search.burst_factor)
extra_posts = extra_posts.where("posts.topic_id NOT in (?)", @results.posts.map(&:topic_id)) if @results.posts.present?
extra_posts.each do |post|
@results.add(post)
expected_topics -= 1
break if expected_topics == 0
end
end
end
# If we're searching for a single topic
def single_topic(id)
post = Post.find_by(topic_id: id, post_number: 1)
return nil unless @guardian.can_see?(post)
@results.add(post)
@results
end
def secure_category_ids
return @secure_category_ids unless @secure_category_ids.nil?
@secure_category_ids = @guardian.secure_category_ids
end
def category_search
# scope is leaking onto Category, this is not good and probably a bug in Rails
# the secure_category_ids will invoke the same method on User, it calls Category.where
# however the scope from the query below is leaking in to Category, this works around
# the issue while we figure out what is up in Rails
secure_category_ids
categories = Category.includes(:category_search_data)
.where("category_search_data.search_data @@ #{ts_query}")
.references(:category_search_data)
.order("topics_month DESC")
.secured(@guardian)
.limit(limit)
categories.each do |category|
@results.add(category)
end
end
def user_search
return if SiteSetting.hide_user_profiles_from_public && !@guardian.user
users = User.includes(:user_search_data)
.references(:user_search_data)
.where(active: true)
.where(staged: false)
.where("user_search_data.search_data @@ #{ts_query("simple")}")
.order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END")
.order("last_posted_at DESC")
.limit(limit)
users.each do |user|
@results.add(user)
end
end
def tags_search
return unless SiteSetting.tagging_enabled
tags = Tag.includes(:tag_search_data)
.where("tag_search_data.search_data @@ #{ts_query}")
.references(:tag_search_data)
.order("name asc")
.limit(limit)
tags.each do |tag|
@results.add(tag)
end
end
def posts_query(limit, opts = nil)
opts ||= {}
posts = Post.where(post_type: Topic.visible_post_types(@guardian.user))
.joins(:post_search_data, :topic)
.joins("LEFT JOIN categories ON categories.id = topics.category_id")
.where("topics.deleted_at" => nil)
is_topic_search = @search_context.present? && @search_context.is_a?(Topic)
posts = posts.where("topics.visible") unless is_topic_search
if opts[:private_messages] || (is_topic_search && @search_context.private_message?)
posts = posts.where("topics.archetype = ?", Archetype.private_message)
unless @guardian.is_admin?
posts = posts.private_posts_for_user(@guardian.user)
end
else
posts = posts.where("topics.archetype <> ?", Archetype.private_message)
end
if @term.present?
if is_topic_search
term_without_quote = @term
if @term =~ /"(.+)"/
term_without_quote = $1
end end
topic_search
end
add_more_topics_if_expected if @term =~ /'(.+)'/
@results term_without_quote = $1
rescue ActiveRecord::StatementInvalid
# In the event of a PG:Error return nothing, it is likely they used a foreign language whose
# locale is not supported by postgres
end
# Add more topics if we expected them
def add_more_topics_if_expected
expected_topics = 0
expected_topics = Search.facets.size unless @results.type_filter.present?
expected_topics = Search.per_facet * Search.facets.size if @results.type_filter == 'topic'
expected_topics -= @results.posts.length
if expected_topics > 0
extra_posts = posts_query(expected_topics * Search.burst_factor)
extra_posts = extra_posts.where("posts.topic_id NOT in (?)", @results.posts.map(&:topic_id)) if @results.posts.present?
extra_posts.each do |post|
@results.add(post)
expected_topics -= 1
break if expected_topics == 0
end end
end
end
# If we're searching for a single topic posts = posts.joins('JOIN users u ON u.id = posts.user_id')
def single_topic(id) posts = posts.where("posts.raw || ' ' || u.username || ' ' || COALESCE(u.name, '') ilike ?", "%#{term_without_quote}%")
post = Post.find_by(topic_id: id, post_number: 1)
return nil unless @guardian.can_see?(post)
@results.add(post)
@results
end
def secure_category_ids
return @secure_category_ids unless @secure_category_ids.nil?
@secure_category_ids = @guardian.secure_category_ids
end
def category_search
# scope is leaking onto Category, this is not good and probably a bug in Rails
# the secure_category_ids will invoke the same method on User, it calls Category.where
# however the scope from the query below is leaking in to Category, this works around
# the issue while we figure out what is up in Rails
secure_category_ids
categories = Category.includes(:category_search_data)
.where("category_search_data.search_data @@ #{ts_query}")
.references(:category_search_data)
.order("topics_month DESC")
.secured(@guardian)
.limit(limit)
categories.each do |category|
@results.add(category)
end
end
def user_search
return if SiteSetting.hide_user_profiles_from_public && !@guardian.user
users = User.includes(:user_search_data)
.references(:user_search_data)
.where(active: true)
.where(staged: false)
.where("user_search_data.search_data @@ #{ts_query("simple")}")
.order("CASE WHEN username_lower = '#{@original_term.downcase}' THEN 0 ELSE 1 END")
.order("last_posted_at DESC")
.limit(limit)
users.each do |user|
@results.add(user)
end
end
def tags_search
return unless SiteSetting.tagging_enabled
tags = Tag.includes(:tag_search_data)
.where("tag_search_data.search_data @@ #{ts_query}")
.references(:tag_search_data)
.order("name asc")
.limit(limit)
tags.each do |tag|
@results.add(tag)
end
end
def posts_query(limit, opts = nil)
opts ||= {}
posts = Post.where(post_type: Topic.visible_post_types(@guardian.user))
.joins(:post_search_data, :topic)
.joins("LEFT JOIN categories ON categories.id = topics.category_id")
.where("topics.deleted_at" => nil)
is_topic_search = @search_context.present? && @search_context.is_a?(Topic)
posts = posts.where("topics.visible") unless is_topic_search
if opts[:private_messages] || (is_topic_search && @search_context.private_message?)
posts = posts.where("topics.archetype = ?", Archetype.private_message)
unless @guardian.is_admin?
posts = posts.private_posts_for_user(@guardian.user)
end
else else
posts = posts.where("topics.archetype <> ?", Archetype.private_message) # A is for title
end # B is for category
# C is for tags
if @term.present? # D is for cooked
if is_topic_search weights = @in_title ? 'A' : (SiteSetting.tagging_enabled ? 'ABCD' : 'ABD')
posts = posts.where("post_search_data.search_data @@ #{ts_query(weight_filter: weights)}")
term_without_quote = @term exact_terms = @term.scan(/"([^"]+)"/).flatten
if @term =~ /"(.+)"/ exact_terms.each do |exact|
term_without_quote = $1 posts = posts.where("posts.raw ilike :exact OR topics.title ilike :exact", exact: "%#{exact}%")
end
if @term =~ /'(.+)'/
term_without_quote = $1
end
posts = posts.joins('JOIN users u ON u.id = posts.user_id')
posts = posts.where("posts.raw || ' ' || u.username || ' ' || COALESCE(u.name, '') ilike ?", "%#{term_without_quote}%")
else
# A is for title
# B is for category
# C is for tags
# D is for cooked
weights = @in_title ? 'A' : (SiteSetting.tagging_enabled ? 'ABCD' : 'ABD')
posts = posts.where("post_search_data.search_data @@ #{ts_query(weight_filter: weights)}")
exact_terms = @term.scan(/"([^"]+)"/).flatten
exact_terms.each do |exact|
posts = posts.where("posts.raw ilike :exact OR topics.title ilike :exact", exact: "%#{exact}%")
end
end end
end end
end
@filters.each do |block, match| @filters.each do |block, match|
if block.arity == 1 if block.arity == 1
posts = instance_exec(posts, &block) || posts posts = instance_exec(posts, &block) || posts
else
posts = instance_exec(posts, match, &block) || posts
end
end if @filters
# If we have a search context, prioritize those posts first
if @search_context.present?
if @search_context.is_a?(User)
if opts[:private_messages]
posts = posts.private_posts_for_user(@search_context)
else
posts = posts.where("posts.user_id = #{@search_context.id}")
end
elsif @search_context.is_a?(Category)
category_ids = [@search_context.id] + Category.where(parent_category_id: @search_context.id).pluck(:id)
posts = posts.where("topics.category_id in (?)", category_ids)
elsif @search_context.is_a?(Topic)
posts = posts.where("topics.id = #{@search_context.id}")
.order("posts.post_number #{@order == :latest ? "DESC" : ""}")
end
end
if @order == :latest || (@term.blank? && !@order)
if opts[:aggregate_search]
posts = posts.order("MAX(posts.created_at) DESC")
else
posts = posts.reorder("posts.created_at DESC")
end
elsif @order == :latest_topic
if opts[:aggregate_search]
posts = posts.order("MAX(topics.created_at) DESC")
else
posts = posts.order("topics.created_at DESC")
end
elsif @order == :views
if opts[:aggregate_search]
posts = posts.order("MAX(topics.views) DESC")
else
posts = posts.order("topics.views DESC")
end
elsif @order == :likes
if opts[:aggregate_search]
posts = posts.order("MAX(posts.like_count) DESC")
else
posts = posts.order("posts.like_count DESC")
end
else else
data_ranking = "TS_RANK_CD(post_search_data.search_data, #{ts_query})" posts = instance_exec(posts, match, &block) || posts
if opts[:aggregate_search] end
posts = posts.order("MAX(#{data_ranking}) DESC") end if @filters
# If we have a search context, prioritize those posts first
if @search_context.present?
if @search_context.is_a?(User)
if opts[:private_messages]
posts = posts.private_posts_for_user(@search_context)
else else
posts = posts.order("#{data_ranking} DESC") posts = posts.where("posts.user_id = #{@search_context.id}")
end end
posts = posts.order("topics.bumped_at DESC")
elsif @search_context.is_a?(Category)
category_ids = [@search_context.id] + Category.where(parent_category_id: @search_context.id).pluck(:id)
posts = posts.where("topics.category_id in (?)", category_ids)
elsif @search_context.is_a?(Topic)
posts = posts.where("topics.id = #{@search_context.id}")
.order("posts.post_number #{@order == :latest ? "DESC" : ""}")
end end
if secure_category_ids.present? end
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?))", secure_category_ids).references(:categories)
if @order == :latest || (@term.blank? && !@order)
if opts[:aggregate_search]
posts = posts.order("MAX(posts.created_at) DESC")
else else
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)").references(:categories) posts = posts.reorder("posts.created_at DESC")
end end
elsif @order == :latest_topic
posts = posts.offset(offset) if opts[:aggregate_search]
posts.limit(limit) posts = posts.order("MAX(topics.created_at) DESC")
end
def self.default_ts_config
"'#{Search.ts_config}'"
end
def default_ts_config
self.class.default_ts_config
end
def self.ts_query(term: , ts_config: nil, joiner: "&", weight_filter: nil)
data = Post.exec_sql("SELECT TO_TSVECTOR(:config, :term)",
config: 'simple',
term: term).values[0][0]
ts_config = ActiveRecord::Base.connection.quote(ts_config) if ts_config
all_terms = data.scan(/'([^']+)'\:\d+/).flatten
all_terms.map! do |t|
t.split(/[\)\(&']/)[0]
end.compact!
query = ActiveRecord::Base.connection.quote(
all_terms
.map { |t| "'#{PG::Connection.escape_string(t)}':*#{weight_filter}" }
.join(" #{joiner} ")
)
"TO_TSQUERY(#{ts_config || default_ts_config}, #{query})"
end
def ts_query(ts_config = nil, weight_filter: nil)
@ts_query_cache ||= {}
@ts_query_cache["#{ts_config || default_ts_config} #{@term} #{weight_filter}"] ||=
Search.ts_query(term: @term, ts_config: ts_config, weight_filter: weight_filter)
end
def wrap_rows(query)
"SELECT *, row_number() over() row_number FROM (#{query.to_sql}) xxx"
end
def aggregate_post_sql(opts)
min_or_max = @order == :latest ? "max" : "min"
query =
if @order == :likes
# likes are a pain to aggregate so skip
posts_query(limit, private_messages: opts[:private_messages])
.select('topics.id', "posts.post_number")
else
posts_query(limit, aggregate_search: true, private_messages: opts[:private_messages])
.select('topics.id', "#{min_or_max}(posts.post_number) post_number")
.group('topics.id')
end
min_id = Search.min_post_id
if min_id > 0
low_set = query.dup.where("post_search_data.post_id < #{min_id}")
high_set = query.where("post_search_data.post_id >= #{min_id}")
return { default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
end
# double wrapping so we get correct row numbers
{ default: wrap_rows(query) }
end
def aggregate_posts(post_sql)
return [] unless post_sql
posts_eager_loads(Post)
.joins("JOIN (#{post_sql}) x ON x.id = posts.topic_id AND x.post_number = posts.post_number")
.order('row_number')
end
def aggregate_search(opts = {})
post_sql = aggregate_post_sql(opts)
added = 0
aggregate_posts(post_sql[:default]).each do |p|
@results.add(p)
added += 1
end
if added < limit
aggregate_posts(post_sql[:remaining]).each { |p| @results.add(p) }
end
end
def private_messages_search
raise Discourse::InvalidAccess.new("anonymous can not search PMs") unless @guardian.user
aggregate_search(private_messages: true)
end
def topic_search
if @search_context.is_a?(Topic)
posts = posts_eager_loads(posts_query(limit))
.where('posts.topic_id = ?', @search_context.id)
posts.each do |post|
@results.add(post)
end
else else
aggregate_search posts = posts.order("topics.created_at DESC")
end end
elsif @order == :views
if opts[:aggregate_search]
posts = posts.order("MAX(topics.views) DESC")
else
posts = posts.order("topics.views DESC")
end
elsif @order == :likes
if opts[:aggregate_search]
posts = posts.order("MAX(posts.like_count) DESC")
else
posts = posts.order("posts.like_count DESC")
end
else
data_ranking = "TS_RANK_CD(post_search_data.search_data, #{ts_query})"
if opts[:aggregate_search]
posts = posts.order("MAX(#{data_ranking}) DESC")
else
posts = posts.order("#{data_ranking} DESC")
end
posts = posts.order("topics.bumped_at DESC")
end end
def posts_eager_loads(query) if secure_category_ids.present?
query = query.includes(:user) posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted) OR (categories.id IN (?))", secure_category_ids).references(:categories)
topic_eager_loads = [:category] else
posts = posts.where("(categories.id IS NULL) OR (NOT categories.read_restricted)").references(:categories)
end
if SiteSetting.tagging_enabled posts = posts.offset(offset)
topic_eager_loads << :tags posts.limit(limit)
end
def self.default_ts_config
"'#{Search.ts_config}'"
end
def default_ts_config
self.class.default_ts_config
end
def self.ts_query(term: , ts_config: nil, joiner: "&", weight_filter: nil)
data = Post.exec_sql("SELECT TO_TSVECTOR(:config, :term)",
config: 'simple',
term: term).values[0][0]
ts_config = ActiveRecord::Base.connection.quote(ts_config) if ts_config
all_terms = data.scan(/'([^']+)'\:\d+/).flatten
all_terms.map! do |t|
t.split(/[\)\(&']/)[0]
end.compact!
query = ActiveRecord::Base.connection.quote(
all_terms
.map { |t| "'#{PG::Connection.escape_string(t)}':*#{weight_filter}" }
.join(" #{joiner} ")
)
"TO_TSQUERY(#{ts_config || default_ts_config}, #{query})"
end
def ts_query(ts_config = nil, weight_filter: nil)
@ts_query_cache ||= {}
@ts_query_cache["#{ts_config || default_ts_config} #{@term} #{weight_filter}"] ||=
Search.ts_query(term: @term, ts_config: ts_config, weight_filter: weight_filter)
end
def wrap_rows(query)
"SELECT *, row_number() over() row_number FROM (#{query.to_sql}) xxx"
end
def aggregate_post_sql(opts)
min_or_max = @order == :latest ? "max" : "min"
query =
if @order == :likes
# likes are a pain to aggregate so skip
posts_query(limit, private_messages: opts[:private_messages])
.select('topics.id', "posts.post_number")
else
posts_query(limit, aggregate_search: true, private_messages: opts[:private_messages])
.select('topics.id', "#{min_or_max}(posts.post_number) post_number")
.group('topics.id')
end end
query.includes(topic: topic_eager_loads) min_id = Search.min_post_id
if min_id > 0
low_set = query.dup.where("post_search_data.post_id < #{min_id}")
high_set = query.where("post_search_data.post_id >= #{min_id}")
return { default: wrap_rows(high_set), remaining: wrap_rows(low_set) }
end end
# double wrapping so we get correct row numbers
{ default: wrap_rows(query) }
end
def aggregate_posts(post_sql)
return [] unless post_sql
posts_eager_loads(Post)
.joins("JOIN (#{post_sql}) x ON x.id = posts.topic_id AND x.post_number = posts.post_number")
.order('row_number')
end
def aggregate_search(opts = {})
post_sql = aggregate_post_sql(opts)
added = 0
aggregate_posts(post_sql[:default]).each do |p|
@results.add(p)
added += 1
end
if added < limit
aggregate_posts(post_sql[:remaining]).each { |p| @results.add(p) }
end
end
def private_messages_search
raise Discourse::InvalidAccess.new("anonymous can not search PMs") unless @guardian.user
aggregate_search(private_messages: true)
end
def topic_search
if @search_context.is_a?(Topic)
posts = posts_eager_loads(posts_query(limit))
.where('posts.topic_id = ?', @search_context.id)
posts.each do |post|
@results.add(post)
end
else
aggregate_search
end
end
def posts_eager_loads(query)
query = query.includes(:user)
topic_eager_loads = [:category]
if SiteSetting.tagging_enabled
topic_eager_loads << :tags
end
query.includes(topic: topic_eager_loads)
end
end end

View file

@ -423,147 +423,147 @@ class TopicQuery
protected protected
def per_page_setting def per_page_setting
@options[:slow_platform] ? 15 : 30 @options[:slow_platform] ? 15 : 30
end end
def private_messages_for(user, type) def private_messages_for(user, type)
options = @options options = @options
options.reverse_merge!(per_page: per_page_setting) options.reverse_merge!(per_page: per_page_setting)
result = Topic.includes(:tags) result = Topic.includes(:tags)
if type == :group if type == :group
result = result.includes(:allowed_users) result = result.includes(:allowed_users)
result = result.where(" result = result.where("
topics.id IN ( topics.id IN (
SELECT topic_id FROM topic_allowed_groups SELECT topic_id FROM topic_allowed_groups
WHERE ( WHERE (
group_id IN ( group_id IN (
SELECT group_id SELECT group_id
FROM group_users FROM group_users
WHERE user_id = #{user.id.to_i}
OR #{user.staff?}
)
)
AND group_id IN (SELECT id FROM groups WHERE name ilike ?)
)",
@options[:group_name]
)
elsif type == :user
result = result.includes(:allowed_users)
result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user.id.to_i})")
elsif type == :all
result = result.includes(:allowed_users)
result = result.where("topics.id IN (
SELECT topic_id
FROM topic_allowed_users
WHERE user_id = #{user.id.to_i} WHERE user_id = #{user.id.to_i}
UNION ALL OR #{user.staff?}
SELECT topic_id FROM topic_allowed_groups )
WHERE group_id IN ( )
SELECT group_id FROM group_users WHERE user_id = #{user.id.to_i} AND group_id IN (SELECT id FROM groups WHERE name ilike ?)
) )",
)") @options[:group_name]
end )
elsif type == :user
result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{user.id.to_i})") result = result.includes(:allowed_users)
.order("topics.bumped_at DESC") result = result.where("topics.id IN (SELECT topic_id FROM topic_allowed_users WHERE user_id = #{user.id.to_i})")
.private_messages elsif type == :all
result = result.includes(:allowed_users)
result = result.limit(options[:per_page]) unless options[:limit] == false result = result.where("topics.id IN (
result = result.visible if options[:visible] || @user.nil? || @user.regular? SELECT topic_id
FROM topic_allowed_users
if options[:page] WHERE user_id = #{user.id.to_i}
offset = options[:page].to_i * options[:per_page] UNION ALL
result = result.offset(offset) if offset > 0 SELECT topic_id FROM topic_allowed_groups
end WHERE group_id IN (
result SELECT group_id FROM group_users WHERE user_id = #{user.id.to_i}
)
)")
end end
def apply_shared_drafts(result, category_id, options) result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{user.id.to_i})")
drafts_category_id = SiteSetting.shared_drafts_category.to_i .order("topics.bumped_at DESC")
viewing_shared = category_id && category_id == drafts_category_id .private_messages
if guardian.can_create_shared_draft? result = result.limit(options[:per_page]) unless options[:limit] == false
if options[:destination_category_id] result = result.visible if options[:visible] || @user.nil? || @user.regular?
destination_category_id = get_category_id(options[:destination_category_id])
topic_ids = SharedDraft.where(category_id: destination_category_id).pluck(:topic_id) if options[:page]
return result.where(id: topic_ids) offset = options[:page].to_i * options[:per_page]
elsif viewing_shared result = result.offset(offset) if offset > 0
result = result.includes(:shared_draft).references(:shared_draft) end
else result
return result.where('topics.category_id != ?', drafts_category_id) end
end
def apply_shared_drafts(result, category_id, options)
drafts_category_id = SiteSetting.shared_drafts_category.to_i
viewing_shared = category_id && category_id == drafts_category_id
if guardian.can_create_shared_draft?
if options[:destination_category_id]
destination_category_id = get_category_id(options[:destination_category_id])
topic_ids = SharedDraft.where(category_id: destination_category_id).pluck(:topic_id)
return result.where(id: topic_ids)
elsif viewing_shared
result = result.includes(:shared_draft).references(:shared_draft)
else
return result.where('topics.category_id != ?', drafts_category_id)
end end
result
end end
def apply_ordering(result, options) result
sort_column = SORTABLE_MAPPING[options[:order]] || 'default' end
sort_dir = (options[:ascending] == "true") ? "ASC" : "DESC"
# If we are sorting in the default order desc, we should consider including pinned def apply_ordering(result, options)
# topics. Otherwise, just use bumped_at. sort_column = SORTABLE_MAPPING[options[:order]] || 'default'
if sort_column == 'default' sort_dir = (options[:ascending] == "true") ? "ASC" : "DESC"
if sort_dir == 'DESC'
# If something requires a custom order, for example "unread" which sorts the least read # If we are sorting in the default order desc, we should consider including pinned
# to the top, do nothing # topics. Otherwise, just use bumped_at.
return result if options[:unordered] if sort_column == 'default'
end if sort_dir == 'DESC'
sort_column = 'bumped_at' # If something requires a custom order, for example "unread" which sorts the least read
# to the top, do nothing
return result if options[:unordered]
end end
sort_column = 'bumped_at'
# If we are sorting by category, actually use the name
if sort_column == 'category_id'
# TODO forces a table scan, slow
return result.references(:categories).order(TopicQuerySQL.order_by_category_sql(sort_dir))
end
if sort_column == 'op_likes'
return result.includes(:first_post).order("(SELECT like_count FROM posts p3 WHERE p3.topic_id = topics.id AND p3.post_number = 1) #{sort_dir}")
end
if sort_column.start_with?('custom_fields')
field = sort_column.split('.')[1]
return result.order("(SELECT CASE WHEN EXISTS (SELECT true FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = '#{field}') THEN (SELECT value::integer FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = '#{field}') ELSE 0 END) #{sort_dir}")
end
result.order("topics.#{sort_column} #{sort_dir}")
end end
def get_category_id(category_id_or_slug) # If we are sorting by category, actually use the name
return nil unless category_id_or_slug if sort_column == 'category_id'
category_id = category_id_or_slug.to_i # TODO forces a table scan, slow
category_id = Category.where(slug: category_id_or_slug).pluck(:id).first if category_id == 0 return result.references(:categories).order(TopicQuerySQL.order_by_category_sql(sort_dir))
category_id
end end
# Create results based on a bunch of default options if sort_column == 'op_likes'
def default_results(options = {}) return result.includes(:first_post).order("(SELECT like_count FROM posts p3 WHERE p3.topic_id = topics.id AND p3.post_number = 1) #{sort_dir}")
options.reverse_merge!(@options) end
options.reverse_merge!(per_page: per_page_setting)
# Whether to return visible topics if sort_column.start_with?('custom_fields')
options[:visible] = true if @user.nil? || @user.regular? field = sort_column.split('.')[1]
options[:visible] = false if @user && @user.id == options[:filtered_to_user] return result.order("(SELECT CASE WHEN EXISTS (SELECT true FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = '#{field}') THEN (SELECT value::integer FROM topic_custom_fields tcf WHERE tcf.topic_id::integer = topics.id::integer AND tcf.name = '#{field}') ELSE 0 END) #{sort_dir}")
end
# Start with a list of all topics result.order("topics.#{sort_column} #{sort_dir}")
result = Topic.unscoped end
if @user def get_category_id(category_id_or_slug)
result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user.id.to_i})") return nil unless category_id_or_slug
.references('tu') category_id = category_id_or_slug.to_i
end category_id = Category.where(slug: category_id_or_slug).pluck(:id).first if category_id == 0
category_id
end
category_id = get_category_id(options[:category]) # Create results based on a bunch of default options
@options[:category_id] = category_id def default_results(options = {})
if category_id options.reverse_merge!(@options)
if options[:no_subcategories] options.reverse_merge!(per_page: per_page_setting)
result = result.where('categories.id = ?', category_id)
else # Whether to return visible topics
sql = <<~SQL options[:visible] = true if @user.nil? || @user.regular?
options[:visible] = false if @user && @user.id == options[:filtered_to_user]
# Start with a list of all topics
result = Topic.unscoped
if @user
result = result.joins("LEFT OUTER JOIN topic_users AS tu ON (topics.id = tu.topic_id AND tu.user_id = #{@user.id.to_i})")
.references('tu')
end
category_id = get_category_id(options[:category])
@options[:category_id] = category_id
if category_id
if options[:no_subcategories]
result = result.where('categories.id = ?', category_id)
else
sql = <<~SQL
categories.id IN ( categories.id IN (
SELECT c2.id FROM categories c2 WHERE c2.parent_category_id = :category_id SELECT c2.id FROM categories c2 WHERE c2.parent_category_id = :category_id
UNION ALL UNION ALL
@ -573,356 +573,356 @@ class TopicQuery
SELECT c3.topic_id FROM categories c3 WHERE c3.parent_category_id = :category_id SELECT c3.topic_id FROM categories c3 WHERE c3.parent_category_id = :category_id
) )
SQL SQL
result = result.where(sql, category_id: category_id) result = result.where(sql, category_id: category_id)
end end
result = result.references(:categories) result = result.references(:categories)
if !@options[:order] if !@options[:order]
# category default sort order # category default sort order
sort_order, sort_ascending = Category.where(id: category_id).pluck(:sort_order, :sort_ascending).first sort_order, sort_ascending = Category.where(id: category_id).pluck(:sort_order, :sort_ascending).first
if sort_order if sort_order
options[:order] = sort_order options[:order] = sort_order
options[:ascending] = !!sort_ascending ? 'true' : 'false' options[:ascending] = !!sort_ascending ? 'true' : 'false'
end
end end
end end
end
# ALL TAGS: something like this? # ALL TAGS: something like this?
# Topic.joins(:tags).where('tags.name in (?)', @options[:tags]).group('topic_id').having('count(*)=?', @options[:tags].size).select('topic_id') # Topic.joins(:tags).where('tags.name in (?)', @options[:tags]).group('topic_id').having('count(*)=?', @options[:tags].size).select('topic_id')
if SiteSetting.tagging_enabled if SiteSetting.tagging_enabled
result = result.preload(:tags) result = result.preload(:tags)
if @options[:tags] && @options[:tags].size > 0 if @options[:tags] && @options[:tags].size > 0
if @options[:match_all_tags] if @options[:match_all_tags]
# ALL of the given tags: # ALL of the given tags:
tags_count = @options[:tags].length tags_count = @options[:tags].length
@options[:tags] = Tag.where(name: @options[:tags]).pluck(:id) unless @options[:tags][0].is_a?(Integer) @options[:tags] = Tag.where(name: @options[:tags]).pluck(:id) unless @options[:tags][0].is_a?(Integer)
if tags_count == @options[:tags].length if tags_count == @options[:tags].length
@options[:tags].each_with_index do |tag, index| @options[:tags].each_with_index do |tag, index|
sql_alias = ['t', index].join sql_alias = ['t', index].join
result = result.joins("INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag}") result = result.joins("INNER JOIN topic_tags #{sql_alias} ON #{sql_alias}.topic_id = topics.id AND #{sql_alias}.tag_id = #{tag}")
end
else
result = result.none # don't return any results unless all tags exist in the database
end end
else else
# ANY of the given tags: result = result.none # don't return any results unless all tags exist in the database
result = result.joins(:tags)
if @options[:tags][0].is_a?(Integer)
result = result.where("tags.id in (?)", @options[:tags])
else
result = result.where("tags.name in (?)", @options[:tags])
end
end end
elsif @options[:no_tags] else
# the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags")) # ANY of the given tags:
result = result.where.not(id: TopicTag.distinct.pluck(:topic_id)) result = result.joins(:tags)
end if @options[:tags][0].is_a?(Integer)
end result = result.where("tags.id in (?)", @options[:tags])
else
result = apply_ordering(result, options) result = result.where("tags.name in (?)", @options[:tags])
result = result.listable_topics.includes(:category)
result = apply_shared_drafts(result, category_id, options)
if options[:exclude_category_ids] && options[:exclude_category_ids].is_a?(Array) && options[:exclude_category_ids].size > 0
result = result.where("categories.id NOT IN (?)", options[:exclude_category_ids]).references(:categories)
end
# Don't include the category topics if excluded
if options[:no_definitions]
result = result.where('COALESCE(categories.topic_id, 0) <> topics.id')
end
result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible]
result = result.where.not(topics: { id: options[:except_topic_ids] }).references(:topics) if options[:except_topic_ids]
if options[:page]
offset = options[:page].to_i * options[:per_page]
result = result.offset(offset) if offset > 0
end
if options[:topic_ids]
result = result.where('topics.id in (?)', options[:topic_ids]).references(:topics)
end
if search = options[:search]
result = result.where("topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{Search.ts_query(term: search.to_s)})")
end
# NOTE protect against SYM attack can be removed with Ruby 2.2
#
state = options[:state]
if @user && state &&
TopicUser.notification_levels.keys.map(&:to_s).include?(state)
level = TopicUser.notification_levels[state.to_sym]
result = result.where('topics.id IN (
SELECT topic_id
FROM topic_users
WHERE user_id = ? AND
notification_level = ?)', @user.id, level)
end
require_deleted_clause = true
if before = options[:before]
if (before = before.to_i) > 0
result = result.where('topics.created_at < ?', before.to_i.days.ago)
end
end
if bumped_before = options[:bumped_before]
if (bumped_before = bumped_before.to_i) > 0
result = result.where('topics.bumped_at < ?', bumped_before.to_i.days.ago)
end
end
if status = options[:status]
case status
when 'open'
result = result.where('NOT topics.closed AND NOT topics.archived')
when 'closed'
result = result.where('topics.closed')
when 'archived'
result = result.where('topics.archived')
when 'listed'
result = result.where('topics.visible')
when 'unlisted'
result = result.where('NOT topics.visible')
when 'deleted'
guardian = @guardian
if guardian.is_staff?
result = result.where('topics.deleted_at IS NOT NULL')
require_deleted_clause = false
end end
end end
elsif @options[:no_tags]
# the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags"))
result = result.where.not(id: TopicTag.distinct.pluck(:topic_id))
end end
if (filter = options[:filter]) && @user
action =
if filter == "bookmarked"
PostActionType.types[:bookmark]
elsif filter == "liked"
PostActionType.types[:like]
end
if action
result = result.where('topics.id IN (SELECT pp.topic_id
FROM post_actions pa
JOIN posts pp ON pp.id = pa.post_id
WHERE pa.user_id = :user_id AND
pa.post_action_type_id = :action AND
pa.deleted_at IS NULL
)', user_id: @user.id,
action: action
)
end
end
result = result.where('topics.deleted_at IS NULL') if require_deleted_clause
result = result.where('topics.posts_count <= ?', options[:max_posts]) if options[:max_posts].present?
result = result.where('topics.posts_count >= ?', options[:min_posts]) if options[:min_posts].present?
result = TopicQuery.apply_custom_filters(result, self)
@guardian.filter_allowed_categories(result)
end end
def remove_muted_topics(list, user) result = apply_ordering(result, options)
if user result = result.listable_topics.includes(:category)
list = list.where('COALESCE(tu.notification_level,1) > :muted', muted: TopicUser.notification_levels[:muted]) result = apply_shared_drafts(result, category_id, options)
end
if options[:exclude_category_ids] && options[:exclude_category_ids].is_a?(Array) && options[:exclude_category_ids].size > 0
result = result.where("categories.id NOT IN (?)", options[:exclude_category_ids]).references(:categories)
end
# Don't include the category topics if excluded
if options[:no_definitions]
result = result.where('COALESCE(categories.topic_id, 0) <> topics.id')
end
result = result.limit(options[:per_page]) unless options[:limit] == false
result = result.visible if options[:visible]
result = result.where.not(topics: { id: options[:except_topic_ids] }).references(:topics) if options[:except_topic_ids]
if options[:page]
offset = options[:page].to_i * options[:per_page]
result = result.offset(offset) if offset > 0
end
if options[:topic_ids]
result = result.where('topics.id in (?)', options[:topic_ids]).references(:topics)
end
if search = options[:search]
result = result.where("topics.id in (select pp.topic_id from post_search_data pd join posts pp on pp.id = pd.post_id where pd.search_data @@ #{Search.ts_query(term: search.to_s)})")
end
# NOTE protect against SYM attack can be removed with Ruby 2.2
#
state = options[:state]
if @user && state &&
TopicUser.notification_levels.keys.map(&:to_s).include?(state)
level = TopicUser.notification_levels[state.to_sym]
result = result.where('topics.id IN (
SELECT topic_id
FROM topic_users
WHERE user_id = ? AND
notification_level = ?)', @user.id, level)
end
require_deleted_clause = true
if before = options[:before]
if (before = before.to_i) > 0
result = result.where('topics.created_at < ?', before.to_i.days.ago)
end
end
if bumped_before = options[:bumped_before]
if (bumped_before = bumped_before.to_i) > 0
result = result.where('topics.bumped_at < ?', bumped_before.to_i.days.ago)
end
end
if status = options[:status]
case status
when 'open'
result = result.where('NOT topics.closed AND NOT topics.archived')
when 'closed'
result = result.where('topics.closed')
when 'archived'
result = result.where('topics.archived')
when 'listed'
result = result.where('topics.visible')
when 'unlisted'
result = result.where('NOT topics.visible')
when 'deleted'
guardian = @guardian
if guardian.is_staff?
result = result.where('topics.deleted_at IS NOT NULL')
require_deleted_clause = false
end
end
end
if (filter = options[:filter]) && @user
action =
if filter == "bookmarked"
PostActionType.types[:bookmark]
elsif filter == "liked"
PostActionType.types[:like]
end
if action
result = result.where('topics.id IN (SELECT pp.topic_id
FROM post_actions pa
JOIN posts pp ON pp.id = pa.post_id
WHERE pa.user_id = :user_id AND
pa.post_action_type_id = :action AND
pa.deleted_at IS NULL
)', user_id: @user.id,
action: action
)
end
end
result = result.where('topics.deleted_at IS NULL') if require_deleted_clause
result = result.where('topics.posts_count <= ?', options[:max_posts]) if options[:max_posts].present?
result = result.where('topics.posts_count >= ?', options[:min_posts]) if options[:min_posts].present?
result = TopicQuery.apply_custom_filters(result, self)
@guardian.filter_allowed_categories(result)
end
def remove_muted_topics(list, user)
if user
list = list.where('COALESCE(tu.notification_level,1) > :muted', muted: TopicUser.notification_levels[:muted])
end
list
end
def remove_muted_categories(list, user, opts = nil)
category_id = get_category_id(opts[:exclude]) if opts
if user
list = list.references("cu")
.where("
NOT EXISTS (
SELECT 1
FROM category_users cu
WHERE cu.user_id = :user_id
AND cu.category_id = topics.category_id
AND cu.notification_level = :muted
AND cu.category_id <> :category_id
AND (tu.notification_level IS NULL OR tu.notification_level < :tracking)
)", user_id: user.id,
muted: CategoryUser.notification_levels[:muted],
tracking: TopicUser.notification_levels[:tracking],
category_id: category_id || -1)
end
list
end
def remove_muted_tags(list, user, opts = nil)
if user.nil? || !SiteSetting.tagging_enabled || !SiteSetting.remove_muted_tags_from_latest
list list
end else
def remove_muted_categories(list, user, opts = nil) if !TagUser.lookup(user, :muted).exists?
category_id = get_category_id(opts[:exclude]) if opts
if user
list = list.references("cu")
.where("
NOT EXISTS (
SELECT 1
FROM category_users cu
WHERE cu.user_id = :user_id
AND cu.category_id = topics.category_id
AND cu.notification_level = :muted
AND cu.category_id <> :category_id
AND (tu.notification_level IS NULL OR tu.notification_level < :tracking)
)", user_id: user.id,
muted: CategoryUser.notification_levels[:muted],
tracking: TopicUser.notification_levels[:tracking],
category_id: category_id || -1)
end
list
end
def remove_muted_tags(list, user, opts = nil)
if user.nil? || !SiteSetting.tagging_enabled || !SiteSetting.remove_muted_tags_from_latest
list list
else else
if !TagUser.lookup(user, :muted).exists? showing_tag = if opts[:filter]
list f = opts[:filter].split('/')
f[0] == 'tags' ? f[1] : nil
else else
showing_tag = if opts[:filter] nil
f = opts[:filter].split('/') end
f[0] == 'tags' ? f[1] : nil
else
nil
end
if TagUser.lookup(user, :muted).joins(:tag).where('tags.name = ?', showing_tag).exists? if TagUser.lookup(user, :muted).joins(:tag).where('tags.name = ?', showing_tag).exists?
list # if viewing the topic list for a muted tag, show all the topics list # if viewing the topic list for a muted tag, show all the topics
else else
muted_tag_ids = TagUser.lookup(user, :muted).pluck(:tag_id) muted_tag_ids = TagUser.lookup(user, :muted).pluck(:tag_id)
list = list.where(" list = list.where("
EXISTS ( EXISTS (
SELECT 1 SELECT 1
FROM topic_tags tt FROM topic_tags tt
WHERE tt.tag_id NOT IN (:tag_ids) WHERE tt.tag_id NOT IN (:tag_ids)
AND tt.topic_id = topics.id AND tt.topic_id = topics.id
) OR NOT EXISTS (SELECT 1 FROM topic_tags tt WHERE tt.topic_id = topics.id)", tag_ids: muted_tag_ids) ) OR NOT EXISTS (SELECT 1 FROM topic_tags tt WHERE tt.topic_id = topics.id)", tag_ids: muted_tag_ids)
end
end end
end end
end end
end
def new_messages(params) def new_messages(params)
TopicQuery.new_filter(messages_for_groups_or_user(params[:my_group_ids]), Time.at(SiteSetting.min_new_topics_time).to_datetime) TopicQuery.new_filter(messages_for_groups_or_user(params[:my_group_ids]), Time.at(SiteSetting.min_new_topics_time).to_datetime)
.limit(params[:count]) .limit(params[:count])
end end
def unread_messages(params) def unread_messages(params)
TopicQuery.unread_filter( TopicQuery.unread_filter(
messages_for_groups_or_user(params[:my_group_ids]), messages_for_groups_or_user(params[:my_group_ids]),
@user&.id, @user&.id,
staff: @user&.staff?) staff: @user&.staff?)
.limit(params[:count]) .limit(params[:count])
end end
def related_messages_user(params) def related_messages_user(params)
messages = messages_for_user.limit(params[:count]) messages = messages_for_user.limit(params[:count])
messages = allowed_messages(messages, params) messages = allowed_messages(messages, params)
end end
def related_messages_group(params) def related_messages_group(params)
messages = messages_for_groups_or_user(params[:my_group_ids]).limit(params[:count]) messages = messages_for_groups_or_user(params[:my_group_ids]).limit(params[:count])
messages = allowed_messages(messages, params) messages = allowed_messages(messages, params)
end end
def allowed_messages(messages, params) def allowed_messages(messages, params)
user_ids = (params[:target_user_ids] || []) user_ids = (params[:target_user_ids] || [])
group_ids = ((params[:target_group_ids] - params[:my_group_ids]) || []) group_ids = ((params[:target_group_ids] - params[:my_group_ids]) || [])
if user_ids.present?
messages =
messages.joins("
LEFT JOIN topic_allowed_users ta2
ON topics.id = ta2.topic_id
AND ta2.user_id IN (#{sanitize_sql_array(user_ids)})
")
end
if group_ids.present?
messages =
messages.joins("
LEFT JOIN topic_allowed_groups tg2
ON topics.id = tg2.topic_id
AND tg2.group_id IN (#{sanitize_sql_array(group_ids)})
")
end
if user_ids.present?
messages = messages =
if user_ids.present? && group_ids.present? messages.joins("
messages.where("ta2.topic_id IS NOT NULL OR tg2.topic_id IS NOT NULL") LEFT JOIN topic_allowed_users ta2
elsif user_ids.present? ON topics.id = ta2.topic_id
messages.where("ta2.topic_id IS NOT NULL") AND ta2.user_id IN (#{sanitize_sql_array(user_ids)})
elsif group_ids.present? ")
messages.where("tg2.topic_id IS NOT NULL")
end
end end
def messages_for_groups_or_user(group_ids) if group_ids.present?
if group_ids.present? messages =
base_messages messages.joins("
.joins(" LEFT JOIN topic_allowed_groups tg2
LEFT JOIN ( ON topics.id = tg2.topic_id
SELECT * FROM topic_allowed_groups _tg AND tg2.group_id IN (#{sanitize_sql_array(group_ids)})
LEFT JOIN group_users gu ")
ON gu.user_id = #{@user.id.to_i} end
AND gu.group_id = _tg.group_id
WHERE gu.group_id IN (#{sanitize_sql_array(group_ids)}) messages =
) tg ON topics.id = tg.topic_id if user_ids.present? && group_ids.present?
") messages.where("ta2.topic_id IS NOT NULL OR tg2.topic_id IS NOT NULL")
.where("tg.topic_id IS NOT NULL") elsif user_ids.present?
else messages.where("ta2.topic_id IS NOT NULL")
messages_for_user elsif group_ids.present?
messages.where("tg2.topic_id IS NOT NULL")
end end
end end
def messages_for_user def messages_for_groups_or_user(group_ids)
if group_ids.present?
base_messages base_messages
.joins(" .joins("
LEFT JOIN topic_allowed_users ta LEFT JOIN (
ON topics.id = ta.topic_id SELECT * FROM topic_allowed_groups _tg
AND ta.user_id = #{@user.id.to_i} LEFT JOIN group_users gu
ON gu.user_id = #{@user.id.to_i}
AND gu.group_id = _tg.group_id
WHERE gu.group_id IN (#{sanitize_sql_array(group_ids)})
) tg ON topics.id = tg.topic_id
") ")
.where("ta.topic_id IS NOT NULL") .where("tg.topic_id IS NOT NULL")
else
messages_for_user
end
end
def messages_for_user
base_messages
.joins("
LEFT JOIN topic_allowed_users ta
ON topics.id = ta.topic_id
AND ta.user_id = #{@user.id.to_i}
")
.where("ta.topic_id IS NOT NULL")
end
def base_messages
query = Topic
.where('topics.archetype = ?', Archetype.private_message)
.joins("LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{@user.id.to_i}")
query = query.includes(:tags) if SiteSetting.tagging_enabled
query.order('topics.bumped_at DESC')
end
def random_suggested(topic, count, excluded_topic_ids = [])
result = default_results(unordered: true, per_page: count).where(closed: false, archived: false)
if SiteSetting.limit_suggested_to_category
excluded_topic_ids += Category.where(id: topic.category_id).pluck(:id)
else
excluded_topic_ids += Category.topic_ids.to_a
end
result = result.where("topics.id NOT IN (?)", excluded_topic_ids) unless excluded_topic_ids.empty?
result = remove_muted_categories(result, @user)
# If we are in a category, prefer it for the random results
if topic.category_id
result = result.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END")
end end
def base_messages # Best effort, it over selects, however if you have a high number
query = Topic # of muted categories there is tiny chance we will not select enough
.where('topics.archetype = ?', Archetype.private_message) # in particular this can happen if current category is empty and tons
.joins("LEFT JOIN topic_users tu ON topics.id = tu.topic_id AND tu.user_id = #{@user.id.to_i}") # of muted, big edge case
#
# we over select in case cache is stale
max = (count * 1.3).to_i
ids = SiteSetting.limit_suggested_to_category ? [] : RandomTopicSelector.next(max)
ids.concat(RandomTopicSelector.next(max, topic.category))
query = query.includes(:tags) if SiteSetting.tagging_enabled result.where(id: ids.uniq)
query.order('topics.bumped_at DESC') end
def suggested_ordering(result, options)
# Prefer unread in the same category
if options[:topic] && options[:topic].category_id
result = result.order("CASE WHEN topics.category_id = #{options[:topic].category_id.to_i} THEN 0 ELSE 1 END")
end end
def random_suggested(topic, count, excluded_topic_ids = []) result.order('topics.bumped_at DESC')
result = default_results(unordered: true, per_page: count).where(closed: false, archived: false) end
if SiteSetting.limit_suggested_to_category
excluded_topic_ids += Category.where(id: topic.category_id).pluck(:id)
else
excluded_topic_ids += Category.topic_ids.to_a
end
result = result.where("topics.id NOT IN (?)", excluded_topic_ids) unless excluded_topic_ids.empty?
result = remove_muted_categories(result, @user)
# If we are in a category, prefer it for the random results
if topic.category_id
result = result.order("CASE WHEN topics.category_id = #{topic.category_id.to_i} THEN 0 ELSE 1 END")
end
# Best effort, it over selects, however if you have a high number
# of muted categories there is tiny chance we will not select enough
# in particular this can happen if current category is empty and tons
# of muted, big edge case
#
# we over select in case cache is stale
max = (count * 1.3).to_i
ids = SiteSetting.limit_suggested_to_category ? [] : RandomTopicSelector.next(max)
ids.concat(RandomTopicSelector.next(max, topic.category))
result.where(id: ids.uniq)
end
def suggested_ordering(result, options)
# Prefer unread in the same category
if options[:topic] && options[:topic].category_id
result = result.order("CASE WHEN topics.category_id = #{options[:topic].category_id.to_i} THEN 0 ELSE 1 END")
end
result.order('topics.bumped_at DESC')
end
private private
def sanitize_sql_array(input) def sanitize_sql_array(input)
ActiveRecord::Base.send(:sanitize_sql_array, input.join(',')) ActiveRecord::Base.send(:sanitize_sql_array, input.join(','))
end end
end end

View file

@ -12,48 +12,48 @@ class TopicRetriever
private private
def invalid_url? def invalid_url?
!EmbeddableHost.url_allowed?(@embed_url) !EmbeddableHost.url_allowed?(@embed_url)
end
def retrieved_recently?
# We can disable the throttle for some users, such as staff
return false if @opts[:no_throttle]
# Throttle other users to once every 60 seconds
retrieved_key = "retrieved_topic"
if $redis.setnx(retrieved_key, "1")
$redis.expire(retrieved_key, 60)
return false
end end
def retrieved_recently? true
# We can disable the throttle for some users, such as staff end
return false if @opts[:no_throttle]
# Throttle other users to once every 60 seconds def perform_retrieve
retrieved_key = "retrieved_topic" # It's possible another process or job found the embed already. So if that happened bail out.
if $redis.setnx(retrieved_key, "1") return if TopicEmbed.where(embed_url: @embed_url).exists?
$redis.expire(retrieved_key, 60)
return false
end
true # First check RSS if that is enabled
end if SiteSetting.feed_polling_enabled?
Jobs::PollFeed.new.execute({})
def perform_retrieve
# It's possible another process or job found the embed already. So if that happened bail out.
return if TopicEmbed.where(embed_url: @embed_url).exists? return if TopicEmbed.where(embed_url: @embed_url).exists?
# First check RSS if that is enabled
if SiteSetting.feed_polling_enabled?
Jobs::PollFeed.new.execute({})
return if TopicEmbed.where(embed_url: @embed_url).exists?
end
fetch_http
end end
def fetch_http fetch_http
if @author_username.nil? end
username = SiteSetting.embed_by_username.downcase
else
username = @author_username
end
user = User.where(username_lower: username.downcase).first def fetch_http
return if user.blank? if @author_username.nil?
username = SiteSetting.embed_by_username.downcase
TopicEmbed.import_remote(user, @embed_url) else
username = @author_username
end end
user = User.where(username_lower: username.downcase).first
return if user.blank?
TopicEmbed.import_remote(user, @embed_url)
end
end end

View file

@ -27,156 +27,156 @@ class TopicsBulkAction
private private
def find_group def find_group
return unless @options[:group] return unless @options[:group]
group = Group.where('name ilike ?', @options[:group]).first group = Group.where('name ilike ?', @options[:group]).first
raise Discourse::InvalidParameters.new(:group) unless group raise Discourse::InvalidParameters.new(:group) unless group
unless group.group_users.where(user_id: @user.id).exists? unless group.group_users.where(user_id: @user.id).exists?
raise Discourse::InvalidParameters.new(:group) raise Discourse::InvalidParameters.new(:group)
end
group
end end
group
end
def move_messages_to_inbox def move_messages_to_inbox
group = find_group group = find_group
topics.each do |t| topics.each do |t|
if guardian.can_see?(t) && t.private_message? if guardian.can_see?(t) && t.private_message?
if group if group
GroupArchivedMessage.move_to_inbox!(group.id, t) GroupArchivedMessage.move_to_inbox!(group.id, t)
else else
UserArchivedMessage.move_to_inbox!(@user.id, t) UserArchivedMessage.move_to_inbox!(@user.id, t)
end
end end
end end
end end
end
def archive_messages def archive_messages
group = find_group group = find_group
topics.each do |t| topics.each do |t|
if guardian.can_see?(t) && t.private_message? if guardian.can_see?(t) && t.private_message?
if group if group
GroupArchivedMessage.archive!(group.id, t) GroupArchivedMessage.archive!(group.id, t)
else else
UserArchivedMessage.archive!(@user.id, t) UserArchivedMessage.archive!(@user.id, t)
end
end end
end end
end end
end
def dismiss_posts def dismiss_posts
sql = " sql = "
UPDATE topic_users tu UPDATE topic_users tu
SET highest_seen_post_number = t.highest_post_number , last_read_post_number = highest_post_number SET highest_seen_post_number = t.highest_post_number , last_read_post_number = highest_post_number
FROM topics t FROM topics t
WHERE t.id = tu.topic_id AND tu.user_id = :user_id AND t.id IN (:topic_ids) WHERE t.id = tu.topic_id AND tu.user_id = :user_id AND t.id IN (:topic_ids)
" "
Topic.exec_sql(sql, user_id: @user.id, topic_ids: @topic_ids) Topic.exec_sql(sql, user_id: @user.id, topic_ids: @topic_ids)
@changed_ids.concat @topic_ids @changed_ids.concat @topic_ids
end end
def reset_read def reset_read
PostTiming.destroy_for(@user.id, @topic_ids) PostTiming.destroy_for(@user.id, @topic_ids)
end end
def change_category def change_category
topics.each do |t| topics.each do |t|
if guardian.can_edit?(t) if guardian.can_edit?(t)
@changed_ids << t.id if t.change_category_to_id(@operation[:category_id]) @changed_ids << t.id if t.change_category_to_id(@operation[:category_id])
end
end end
end end
end
def change_notification_level def change_notification_level
topics.each do |t| topics.each do |t|
if guardian.can_see?(t) if guardian.can_see?(t)
TopicUser.change(@user, t.id, notification_level: @operation[:notification_level_id].to_i) TopicUser.change(@user, t.id, notification_level: @operation[:notification_level_id].to_i)
@changed_ids << t.id
end
end
end
def close
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('closed', true, @user)
@changed_ids << t.id
end
end
end
def unlist
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('visible', false, @user)
@changed_ids << t.id
end
end
end
def relist
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('visible', true, @user)
@changed_ids << t.id
end
end
end
def archive
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('archived', true, @user)
@changed_ids << t.id
end
end
end
def delete
topics.each do |t|
if guardian.can_delete?(t)
PostDestroyer.new(@user, t.ordered_posts.first).destroy
end
end
end
def change_tags
tags = @operation[:tags]
tags = DiscourseTagging.tags_for_saving(tags, guardian) if tags.present?
topics.each do |t|
if guardian.can_edit?(t)
if tags.present?
DiscourseTagging.tag_topic_by_names(t, guardian, tags)
else
t.tags = []
end
@changed_ids << t.id
end
end
end
def append_tags
tags = @operation[:tags]
tags = DiscourseTagging.tags_for_saving(tags, guardian) if tags.present?
topics.each do |t|
if guardian.can_edit?(t)
if tags.present?
DiscourseTagging.tag_topic_by_names(t, guardian, tags, append: true)
end
@changed_ids << t.id @changed_ids << t.id
end
end end
end end
end
def guardian def close
@guardian ||= Guardian.new(@user) topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('closed', true, @user)
@changed_ids << t.id
end
end end
end
def topics def unlist
@topics ||= Topic.where(id: @topic_ids) topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('visible', false, @user)
@changed_ids << t.id
end
end end
end
def relist
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('visible', true, @user)
@changed_ids << t.id
end
end
end
def archive
topics.each do |t|
if guardian.can_moderate?(t)
t.update_status('archived', true, @user)
@changed_ids << t.id
end
end
end
def delete
topics.each do |t|
if guardian.can_delete?(t)
PostDestroyer.new(@user, t.ordered_posts.first).destroy
end
end
end
def change_tags
tags = @operation[:tags]
tags = DiscourseTagging.tags_for_saving(tags, guardian) if tags.present?
topics.each do |t|
if guardian.can_edit?(t)
if tags.present?
DiscourseTagging.tag_topic_by_names(t, guardian, tags)
else
t.tags = []
end
@changed_ids << t.id
end
end
end
def append_tags
tags = @operation[:tags]
tags = DiscourseTagging.tags_for_saving(tags, guardian) if tags.present?
topics.each do |t|
if guardian.can_edit?(t)
if tags.present?
DiscourseTagging.tag_topic_by_names(t, guardian, tags, append: true)
end
@changed_ids << t.id
end
end
end
def guardian
@guardian ||= Guardian.new(@user)
end
def topics
@topics ||= Topic.where(id: @topic_ids)
end
end end

View file

@ -10,23 +10,23 @@ class CensoredWordsValidator < ActiveModel::EachValidator
private private
def censor_words(value, regexp) def censor_words(value, regexp)
censored_words = value.scan(regexp) censored_words = value.scan(regexp)
censored_words.flatten! censored_words.flatten!
censored_words.compact! censored_words.compact!
censored_words.map!(&:strip) censored_words.map!(&:strip)
censored_words.select!(&:present?) censored_words.select!(&:present?)
censored_words.uniq! censored_words.uniq!
censored_words censored_words
end end
def join_censored_words(censored_words) def join_censored_words(censored_words)
censored_words.map!(&:downcase) censored_words.map!(&:downcase)
censored_words.uniq! censored_words.uniq!
censored_words.join(", ".freeze) censored_words.join(", ".freeze)
end end
def censored_words_regexp def censored_words_regexp
WordWatcher.word_matcher_regexp :censor WordWatcher.word_matcher_regexp :censor
end end
end end

View file

@ -30,15 +30,15 @@ class POP3PollingEnabledSettingValidator
private private
def authentication_works? def authentication_works?
@authentication_works ||= begin @authentication_works ||= begin
pop3 = Net::POP3.new(SiteSetting.pop3_polling_host, SiteSetting.pop3_polling_port) pop3 = Net::POP3.new(SiteSetting.pop3_polling_host, SiteSetting.pop3_polling_port)
pop3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if SiteSetting.pop3_polling_ssl pop3.enable_ssl(OpenSSL::SSL::VERIFY_NONE) if SiteSetting.pop3_polling_ssl
pop3.auth_only(SiteSetting.pop3_polling_username, SiteSetting.pop3_polling_password) pop3.auth_only(SiteSetting.pop3_polling_username, SiteSetting.pop3_polling_password)
rescue Net::POPAuthenticationError rescue Net::POPAuthenticationError
false false
else else
true true
end
end end
end
end end

View file

@ -6,17 +6,17 @@ class TopicTitleLengthValidator < ActiveModel::EachValidator
private private
def title_validator(record) def title_validator(record)
length_range = length_range =
if record.user.try(:admin?) if record.user.try(:admin?)
1..SiteSetting.max_topic_title_length 1..SiteSetting.max_topic_title_length
elsif record.private_message? elsif record.private_message?
SiteSetting.private_message_title_length SiteSetting.private_message_title_length
else else
SiteSetting.topic_title_length SiteSetting.topic_title_length
end end
ActiveModel::Validations::LengthValidator.new(attributes: :title, in: length_range, allow_blank: true) ActiveModel::Validations::LengthValidator.new(attributes: :title, in: length_range, allow_blank: true)
end end
end end

View file

@ -563,49 +563,49 @@ module DiscourseNarrativeBot
private private
def name def name
@user.username.titleize @user.username.titleize
end end
def logo_group(size, width, height) def logo_group(size, width, height)
return unless SiteSetting.logo_small_url.present? return unless SiteSetting.logo_small_url.present?
begin begin
uri = URI(SiteSetting.logo_small_url) uri = URI(SiteSetting.logo_small_url)
logo_uri = logo_uri =
if uri.host.blank? || uri.scheme.blank? if uri.host.blank? || uri.scheme.blank?
URI("#{Discourse.base_url}/#{uri.path}") URI("#{Discourse.base_url}/#{uri.path}")
else else
uri uri
end end
<<~URL <<~URL
<g transform="translate(#{width / 2 - (size / 2)} #{height})"> <g transform="translate(#{width / 2 - (size / 2)} #{height})">
<image height="#{size}px" width="#{size}px" #{base64_image_link(logo_uri)}/> <image height="#{size}px" width="#{size}px" #{base64_image_link(logo_uri)}/>
</g> </g>
URL URL
rescue URI::InvalidURIError rescue URI::InvalidURIError
'' ''
end
end end
end
def base64_image_link(url) def base64_image_link(url)
if image = fetch_image(url) if image = fetch_image(url)
"xlink:href=\"data:image/png;base64,#{Base64.strict_encode64(image)}\"" "xlink:href=\"data:image/png;base64,#{Base64.strict_encode64(image)}\""
else else
"" ""
end
end end
end
def fetch_image(url) def fetch_image(url)
URI(url).open('rb', redirect: true, allow_redirections: :all).read URI(url).open('rb', redirect: true, allow_redirections: :all).read
rescue OpenURI::HTTPError rescue OpenURI::HTTPError
# Ignore if fetching image returns a non 200 response # Ignore if fetching image returns a non 200 response
end end
def avatar_url def avatar_url
UrlHelper.absolute(Discourse.base_uri + @user.avatar_template.gsub('{size}', '250')) UrlHelper.absolute(Discourse.base_uri + @user.avatar_template.gsub('{size}', '250'))
end end
end end
end end

View file

@ -8,102 +8,102 @@ class QuandoraQuestion
@question = JSON.parse question_json @question = JSON.parse question_json
end end
def topic def topic
topic = {} topic = {}
topic[:id] = @question['uid'] topic[:id] = @question['uid']
topic[:author_id] = @question['author']['uid'] topic[:author_id] = @question['author']['uid']
topic[:title] = unescape @question['title'] topic[:title] = unescape @question['title']
topic[:raw] = unescape @question['content'] topic[:raw] = unescape @question['content']
topic[:created_at] = Time.parse @question['created'] topic[:created_at] = Time.parse @question['created']
topic topic
end end
def users def users
users = {} users = {}
user = user_from_author @question['author'] user = user_from_author @question['author']
users[user[:id]] = user users[user[:id]] = user
replies.each do |reply| replies.each do |reply|
user = user_from_author reply[:author] user = user_from_author reply[:author]
users[user[:id]] = user users[user[:id]] = user
end end
users.values.to_a users.values.to_a
end end
def user_from_author(author) def user_from_author(author)
email = author['email'] email = author['email']
email = "#{author['uid']}@noemail.com" unless email email = "#{author['uid']}@noemail.com" unless email
user = {} user = {}
user[:id] = author['uid'] user[:id] = author['uid']
user[:name] = "#{author['firstName']} #{author['lastName']}" user[:name] = "#{author['firstName']} #{author['lastName']}"
user[:email] = email user[:email] = email
user[:staged] = true user[:staged] = true
user user
end end
def replies def replies
posts = [] posts = []
answers = @question['answersList'] answers = @question['answersList']
comments = @question['comments'] comments = @question['comments']
comments.each_with_index do |comment, i| comments.each_with_index do |comment, i|
posts << post_from_comment(comment, i, @question) posts << post_from_comment(comment, i, @question)
end end
answers.each do |answer| answers.each do |answer|
posts << post_from_answer(answer) posts << post_from_answer(answer)
comments = answer['comments'] comments = answer['comments']
comments.each_with_index do |comment, i| comments.each_with_index do |comment, i|
posts << post_from_comment(comment, i, answer) posts << post_from_comment(comment, i, answer)
end end
end end
order_replies posts order_replies posts
end end
def order_replies(posts) def order_replies(posts)
posts = posts.sort_by { |p| p[:created_at] } posts = posts.sort_by { |p| p[:created_at] }
posts.each_with_index do |p, i| posts.each_with_index do |p, i|
p[:post_number] = i + 2 p[:post_number] = i + 2
end end
posts.each do |p| posts.each do |p|
parent = posts.select { |pp| pp[:id] == p[:parent_id] } parent = posts.select { |pp| pp[:id] == p[:parent_id] }
p[:reply_to_post_number] = parent[0][:post_number] if parent.size > 0 p[:reply_to_post_number] = parent[0][:post_number] if parent.size > 0
end end
posts posts
end end
def post_from_answer(answer) def post_from_answer(answer)
post = {} post = {}
post[:id] = answer['uid'] post[:id] = answer['uid']
post[:parent_id] = @question['uid'] post[:parent_id] = @question['uid']
post[:author] = answer['author'] post[:author] = answer['author']
post[:author_id] = answer['author']['uid'] post[:author_id] = answer['author']['uid']
post[:raw] = unescape answer['content'] post[:raw] = unescape answer['content']
post[:created_at] = Time.parse answer['created'] post[:created_at] = Time.parse answer['created']
post post
end end
def post_from_comment(comment, index, parent) def post_from_comment(comment, index, parent)
if comment['created'] if comment['created']
created_at = Time.parse comment['created'] created_at = Time.parse comment['created']
else else
created_at = Time.parse parent['created'] created_at = Time.parse parent['created']
end end
parent_id = parent['uid'] parent_id = parent['uid']
parent_id = "#{parent['uid']}-#{index - 1}" if index > 0 parent_id = "#{parent['uid']}-#{index - 1}" if index > 0
post = {} post = {}
id = "#{parent['uid']}-#{index}" id = "#{parent['uid']}-#{index}"
post[:id] = id post[:id] = id
post[:parent_id] = parent_id post[:parent_id] = parent_id
post[:author] = comment['author'] post[:author] = comment['author']
post[:author_id] = comment['author']['uid'] post[:author_id] = comment['author']['uid']
post[:raw] = unescape comment['text'] post[:raw] = unescape comment['text']
post[:created_at] = created_at post[:created_at] = created_at
post post
end end
private private
def unescape(html) def unescape(html)
return nil unless html return nil unless html
CGI.unescapeHTML html CGI.unescapeHTML html
end end
end end

View file

@ -18,81 +18,81 @@ class SocialcastMessage
} }
} }
def initialize(message_json) def initialize(message_json)
@parsed_json = JSON.parse message_json @parsed_json = JSON.parse message_json
end end
def topic def topic
topic = {} topic = {}
topic[:id] = @parsed_json['id'] topic[:id] = @parsed_json['id']
topic[:author_id] = @parsed_json['user']['id'] topic[:author_id] = @parsed_json['user']['id']
topic[:title] = title topic[:title] = title
topic[:raw] = @parsed_json['body'] topic[:raw] = @parsed_json['body']
topic[:created_at] = Time.parse @parsed_json['created_at'] topic[:created_at] = Time.parse @parsed_json['created_at']
topic[:tags] = tags topic[:tags] = tags
topic[:category] = category topic[:category] = category
topic topic
end end
def title def title
CreateTitle.from_body @parsed_json['body'] CreateTitle.from_body @parsed_json['body']
end end
def tags def tags
tags = [] tags = []
if group if group
if TAGS_AND_CATEGORIES[group] if TAGS_AND_CATEGORIES[group]
tags = TAGS_AND_CATEGORIES[group][:tags] tags = TAGS_AND_CATEGORIES[group][:tags]
else else
tags << group tags << group
end end
end end
tags << DEFAULT_TAG tags << DEFAULT_TAG
tags tags
end end
def category def category
category = DEFAULT_CATEGORY category = DEFAULT_CATEGORY
if group && TAGS_AND_CATEGORIES[group] if group && TAGS_AND_CATEGORIES[group]
category = TAGS_AND_CATEGORIES[group][:category] category = TAGS_AND_CATEGORIES[group][:category]
end end
category category
end end
def group def group
@parsed_json['group']['groupname'].downcase if @parsed_json['group'] && @parsed_json['group']['groupname'] @parsed_json['group']['groupname'].downcase if @parsed_json['group'] && @parsed_json['group']['groupname']
end end
def url def url
@parsed_json['url'] @parsed_json['url']
end end
def message_type def message_type
@parsed_json['message_type'] @parsed_json['message_type']
end end
def replies def replies
posts = [] posts = []
comments = @parsed_json['comments'] comments = @parsed_json['comments']
comments.each do |comment| comments.each do |comment|
posts << post_from_comment(comment) posts << post_from_comment(comment)
end end
posts posts
end end
def post_from_comment(comment) def post_from_comment(comment)
post = {} post = {}
post[:id] = comment['id'] post[:id] = comment['id']
post[:author_id] = comment['user']['id'] post[:author_id] = comment['user']['id']
post[:raw] = comment['text'] post[:raw] = comment['text']
post[:created_at] = Time.parse comment['created_at'] post[:created_at] = Time.parse comment['created_at']
post post
end end
private private
def unescape(html) def unescape(html)
return nil unless html return nil unless html
CGI.unescapeHTML html CGI.unescapeHTML html
end end
end end

View file

@ -8,17 +8,17 @@ class SocialcastUser
@parsed_json = JSON.parse user_json @parsed_json = JSON.parse user_json
end end
def user def user
email = @parsed_json['contact_info']['email'] email = @parsed_json['contact_info']['email']
email = "#{@parsed_json['id']}@noemail.com" unless email email = "#{@parsed_json['id']}@noemail.com" unless email
user = {} user = {}
user[:id] = @parsed_json['id'] user[:id] = @parsed_json['id']
user[:name] = @parsed_json['name'] user[:name] = @parsed_json['name']
user[:username] = @parsed_json['username'] user[:username] = @parsed_json['username']
user[:email] = email user[:email] = email
user[:staged] = true user[:staged] = true
user user
end end
end end

View file

@ -28,226 +28,226 @@ class ImportScripts::Vanilla < ImportScripts::Base
private private
def check_file_exist def check_file_exist
raise ArgumentError.new("File does not exist: #{@vanilla_file}") unless File.exist?(@vanilla_file) raise ArgumentError.new("File does not exist: #{@vanilla_file}") unless File.exist?(@vanilla_file)
end end
def parse_file def parse_file
puts "parsing file..." puts "parsing file..."
file = read_file file = read_file
# TODO: parse header & validate version number # TODO: parse header & validate version number
header = file.readline header = file.readline
until file.eof? until file.eof?
line = file.readline line = file.readline
next if line.blank? next if line.blank?
next if line.start_with?("//") next if line.start_with?("//")
if m = /^Table: (\w+)/.match(line) if m = /^Table: (\w+)/.match(line)
# extract table name # extract table name
table = m[1].underscore.pluralize table = m[1].underscore.pluralize
# read the data until an empty line # read the data until an empty line
data = [] data = []
# first line is the table definition, turn that into a proper csv header # first line is the table definition, turn that into a proper csv header
data << file.readline.split(",").map { |c| c.split(":")[0].underscore }.join(",") data << file.readline.split(",").map { |c| c.split(":")[0].underscore }.join(",")
until (line = file.readline).blank? until (line = file.readline).blank?
data << line.strip data << line.strip
end
# PERF: don't parse useless tables
useless_tables = ["user_meta"]
useless_tables << "activities" unless @use_lastest_activity_as_user_bio
next if useless_tables.include?(table)
# parse the data
puts "parsing #{table}..."
parsed_data = CSV.parse(data.join("\n"), headers: true, header_converters: :symbol).map { |row| row.to_hash }
instance_variable_set("@#{table}".to_sym, parsed_data)
end end
# PERF: don't parse useless tables
useless_tables = ["user_meta"]
useless_tables << "activities" unless @use_lastest_activity_as_user_bio
next if useless_tables.include?(table)
# parse the data
puts "parsing #{table}..."
parsed_data = CSV.parse(data.join("\n"), headers: true, header_converters: :symbol).map { |row| row.to_hash }
instance_variable_set("@#{table}".to_sym, parsed_data)
end end
end end
end
def read_file def read_file
puts "reading file..." puts "reading file..."
string = File.read(@vanilla_file).gsub("\\N", "") string = File.read(@vanilla_file).gsub("\\N", "")
.gsub(/\\$\n/m, "\\n") .gsub(/\\$\n/m, "\\n")
.gsub("\\,", ",") .gsub("\\,", ",")
.gsub(/(?<!\\)\\"/, '""') .gsub(/(?<!\\)\\"/, '""')
.gsub(/\\\\\\"/, '\\""') .gsub(/\\\\\\"/, '\\""')
StringIO.new(string) StringIO.new(string)
end end
def import_users def import_users
puts "", "importing users..." puts "", "importing users..."
admin_role_id = @roles.select { |r| r[:name] == "Administrator" }.first[:role_id] admin_role_id = @roles.select { |r| r[:name] == "Administrator" }.first[:role_id]
moderator_role_id = @roles.select { |r| r[:name] == "Moderator" }.first[:role_id] moderator_role_id = @roles.select { |r| r[:name] == "Moderator" }.first[:role_id]
activities = (@activities || []).reject { |a| a[:activity_user_id] != a[:regarding_user_id] } activities = (@activities || []).reject { |a| a[:activity_user_id] != a[:regarding_user_id] }
create_users(@users) do |user| create_users(@users) do |user|
next if user[:name] == "[Deleted User]" next if user[:name] == "[Deleted User]"
if @use_lastest_activity_as_user_bio if @use_lastest_activity_as_user_bio
last_activity = activities.select { |a| user[:user_id] == a[:activity_user_id] }.last last_activity = activities.select { |a| user[:user_id] == a[:activity_user_id] }.last
bio_raw = last_activity.try(:[], :story) || "" bio_raw = last_activity.try(:[], :story) || ""
else else
bio_raw = user[:discovery_text] bio_raw = user[:discovery_text]
end
u = {
id: user[:user_id],
email: user[:email],
username: user[:name],
created_at: parse_date(user[:date_inserted]),
bio_raw: clean_up(bio_raw),
avatar_url: user[:photo],
moderator: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(moderator_role_id),
admin: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(admin_role_id),
}
u
end end
end
def import_categories u = {
puts "", "importing categories..." id: user[:user_id],
email: user[:email],
# save some information about the root category username: user[:name],
@root_category = @categories.select { |c| c[:category_id] == "-1" }.first created_at: parse_date(user[:date_inserted]),
@root_category_created_at = parse_date(@root_category[:date_inserted]) bio_raw: clean_up(bio_raw),
avatar_url: user[:photo],
# removes root category moderator: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(moderator_role_id),
@categories.reject! { |c| c[:category_id] == "-1" } admin: @user_roles.select { |ur| ur[:user_id] == user[:user_id] }.map { |ur| ur[:role_id] }.include?(admin_role_id),
# adds root's child categories
first_level_categories = @categories.select { |c| c[:parent_category_id] == "-1" }
if first_level_categories.count > 0
puts "", "importing first-level categories..."
create_categories(first_level_categories) { |category| import_category(category) }
# adds other categories
second_level_categories = @categories.select { |c| c[:parent_category_id] != "-1" }
if second_level_categories.count > 0
puts "", "importing second-level categories..."
create_categories(second_level_categories) { |category| import_category(category) }
end
end
end
def import_category(category)
c = {
id: category[:category_id],
name: category[:name],
user_id: user_id_from_imported_user_id(category[:insert_user_id]) || Discourse::SYSTEM_USER_ID,
position: category[:sort].to_i,
created_at: parse_category_date(category[:date_inserted]),
description: clean_up(category[:description]),
} }
if category[:parent_category_id] != "-1"
c[:parent_category_id] = category_id_from_imported_category_id(category[:parent_category_id]) u
end
c
end end
end
def parse_category_date(date) def import_categories
date == "0000-00-00 00:00:00" ? @root_category_created_at : parse_date(date) puts "", "importing categories..."
end
def import_topics # save some information about the root category
puts "", "importing topics..." @root_category = @categories.select { |c| c[:category_id] == "-1" }.first
@root_category_created_at = parse_date(@root_category[:date_inserted])
create_posts(@discussions) do |discussion| # removes root category
{ @categories.reject! { |c| c[:category_id] == "-1" }
id: "discussion#" + discussion[:discussion_id],
user_id: user_id_from_imported_user_id(discussion[:insert_user_id]) || Discourse::SYSTEM_USER_ID, # adds root's child categories
title: discussion[:name], first_level_categories = @categories.select { |c| c[:parent_category_id] == "-1" }
category: category_id_from_imported_category_id(discussion[:category_id]), if first_level_categories.count > 0
raw: clean_up(discussion[:body]), puts "", "importing first-level categories..."
created_at: parse_date(discussion[:date_inserted]), create_categories(first_level_categories) { |category| import_category(category) }
}
# adds other categories
second_level_categories = @categories.select { |c| c[:parent_category_id] != "-1" }
if second_level_categories.count > 0
puts "", "importing second-level categories..."
create_categories(second_level_categories) { |category| import_category(category) }
end end
end end
end
def import_posts def import_category(category)
puts "", "importing posts..." c = {
id: category[:category_id],
create_posts(@comments) do |comment| name: category[:name],
next unless t = topic_lookup_from_imported_post_id("discussion#" + comment[:discussion_id]) user_id: user_id_from_imported_user_id(category[:insert_user_id]) || Discourse::SYSTEM_USER_ID,
position: category[:sort].to_i,
{ created_at: parse_category_date(category[:date_inserted]),
id: "comment#" + comment[:comment_id], description: clean_up(category[:description]),
user_id: user_id_from_imported_user_id(comment[:insert_user_id]) || Discourse::SYSTEM_USER_ID, }
topic_id: t[:topic_id], if category[:parent_category_id] != "-1"
raw: clean_up(comment[:body]), c[:parent_category_id] = category_id_from_imported_category_id(category[:parent_category_id])
created_at: parse_date(comment[:date_inserted]),
}
end
end end
c
end
def import_private_topics def parse_category_date(date)
puts "", "importing private topics..." date == "0000-00-00 00:00:00" ? @root_category_created_at : parse_date(date)
end
create_posts(@conversations) do |conversation| def import_topics
next if conversation[:first_message_id].blank? puts "", "importing topics..."
# list all other user ids in the conversation create_posts(@discussions) do |discussion|
user_ids_in_conversation = @user_conversations.select { |uc| uc[:conversation_id] == conversation[:conversation_id] && uc[:user_id] != conversation[:insert_user_id] } {
.map { |uc| uc[:user_id] } id: "discussion#" + discussion[:discussion_id],
# retrieve their emails user_id: user_id_from_imported_user_id(discussion[:insert_user_id]) || Discourse::SYSTEM_USER_ID,
user_emails_in_conversation = @users.select { |u| user_ids_in_conversation.include?(u[:user_id]) } title: discussion[:name],
.map { |u| u[:email] } category: category_id_from_imported_category_id(discussion[:category_id]),
# retrieve their usernames from the database raw: clean_up(discussion[:body]),
target_usernames = User.where("email IN (?)", user_emails_in_conversation).pluck(:username).to_a created_at: parse_date(discussion[:date_inserted]),
}
next if target_usernames.blank?
user = find_user_by_import_id(conversation[:insert_user_id]) || Discourse.system_user
first_message = @conversation_messages.select { |cm| cm[:message_id] == conversation[:first_message_id] }.first
{
archetype: Archetype.private_message,
id: "conversation#" + conversation[:conversation_id],
user_id: user.id,
title: "Private message from #{user.username}",
target_usernames: target_usernames,
raw: clean_up(first_message[:body]),
created_at: parse_date(conversation[:date_inserted]),
}
end
end end
end
def import_private_posts def import_posts
puts "", "importing private posts..." puts "", "importing posts..."
first_message_ids = Set.new(@conversations.map { |c| c[:first_message_id] }.to_a) create_posts(@comments) do |comment|
@conversation_messages.reject! { |cm| first_message_ids.include?(cm[:message_id]) } next unless t = topic_lookup_from_imported_post_id("discussion#" + comment[:discussion_id])
create_posts(@conversation_messages) do |message| {
next unless t = topic_lookup_from_imported_post_id("conversation#" + message[:conversation_id]) id: "comment#" + comment[:comment_id],
user_id: user_id_from_imported_user_id(comment[:insert_user_id]) || Discourse::SYSTEM_USER_ID,
{ topic_id: t[:topic_id],
archetype: Archetype.private_message, raw: clean_up(comment[:body]),
id: "message#" + message[:message_id], created_at: parse_date(comment[:date_inserted]),
user_id: user_id_from_imported_user_id(message[:insert_user_id]) || Discourse::SYSTEM_USER_ID, }
topic_id: t[:topic_id],
raw: clean_up(message[:body]),
created_at: parse_date(message[:date_inserted]),
}
end
end end
end
def parse_date(date) def import_private_topics
DateTime.strptime(date, "%Y-%m-%d %H:%M:%S") puts "", "importing private topics..."
end
def clean_up(raw) create_posts(@conversations) do |conversation|
return "" if raw.blank? next if conversation[:first_message_id].blank?
raw.gsub("\\n", "\n")
.gsub(/<\/?pre\s*>/i, "\n```\n") # list all other user ids in the conversation
.gsub(/<\/?code\s*>/i, "`") user_ids_in_conversation = @user_conversations.select { |uc| uc[:conversation_id] == conversation[:conversation_id] && uc[:user_id] != conversation[:insert_user_id] }
.gsub("&lt;", "<") .map { |uc| uc[:user_id] }
.gsub("&gt;", ">") # retrieve their emails
user_emails_in_conversation = @users.select { |u| user_ids_in_conversation.include?(u[:user_id]) }
.map { |u| u[:email] }
# retrieve their usernames from the database
target_usernames = User.where("email IN (?)", user_emails_in_conversation).pluck(:username).to_a
next if target_usernames.blank?
user = find_user_by_import_id(conversation[:insert_user_id]) || Discourse.system_user
first_message = @conversation_messages.select { |cm| cm[:message_id] == conversation[:first_message_id] }.first
{
archetype: Archetype.private_message,
id: "conversation#" + conversation[:conversation_id],
user_id: user.id,
title: "Private message from #{user.username}",
target_usernames: target_usernames,
raw: clean_up(first_message[:body]),
created_at: parse_date(conversation[:date_inserted]),
}
end end
end
def import_private_posts
puts "", "importing private posts..."
first_message_ids = Set.new(@conversations.map { |c| c[:first_message_id] }.to_a)
@conversation_messages.reject! { |cm| first_message_ids.include?(cm[:message_id]) }
create_posts(@conversation_messages) do |message|
next unless t = topic_lookup_from_imported_post_id("conversation#" + message[:conversation_id])
{
archetype: Archetype.private_message,
id: "message#" + message[:message_id],
user_id: user_id_from_imported_user_id(message[:insert_user_id]) || Discourse::SYSTEM_USER_ID,
topic_id: t[:topic_id],
raw: clean_up(message[:body]),
created_at: parse_date(message[:date_inserted]),
}
end
end
def parse_date(date)
DateTime.strptime(date, "%Y-%m-%d %H:%M:%S")
end
def clean_up(raw)
return "" if raw.blank?
raw.gsub("\\n", "\n")
.gsub(/<\/?pre\s*>/i, "\n```\n")
.gsub(/<\/?code\s*>/i, "`")
.gsub("&lt;", "<")
.gsub("&gt;", ">")
end
end end