mirror of
https://github.com/discourse/discourse.git
synced 2024-11-27 11:20:57 -06:00
30990006a9
This reduces chances of errors where consumers of strings mutate inputs and reduces memory usage of the app. Test suite passes now, but there may be some stuff left, so we will run a few sites on a branch prior to merging
579 lines
16 KiB
Ruby
579 lines
16 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class GroupsController < ApplicationController
|
|
include ApplicationHelper
|
|
|
|
requires_login only: [
|
|
:set_notifications,
|
|
:mentionable,
|
|
:messageable,
|
|
:check_name,
|
|
:update,
|
|
:histories,
|
|
:request_membership,
|
|
:search,
|
|
:new
|
|
]
|
|
|
|
skip_before_action :preload_json, :check_xhr, only: [:posts_feed, :mentions_feed]
|
|
skip_before_action :check_xhr, only: [:show]
|
|
|
|
TYPE_FILTERS = {
|
|
my: Proc.new { |groups, user|
|
|
raise Discourse::NotFound unless user
|
|
Group.member_of(groups, user)
|
|
},
|
|
owner: Proc.new { |groups, user|
|
|
raise Discourse::NotFound unless user
|
|
Group.owner_of(groups, user)
|
|
},
|
|
public: Proc.new { |groups|
|
|
groups.where(public_admission: true, automatic: false)
|
|
},
|
|
close: Proc.new { |groups|
|
|
groups.where(
|
|
public_admission: false,
|
|
automatic: false
|
|
)
|
|
},
|
|
automatic: Proc.new { |groups|
|
|
groups.where(automatic: true)
|
|
}
|
|
}
|
|
|
|
def index
|
|
unless SiteSetting.enable_group_directory? || current_user&.staff?
|
|
raise Discourse::InvalidAccess.new(:enable_group_directory)
|
|
end
|
|
|
|
page_size = mobile_device? ? 15 : 36
|
|
page = params[:page]&.to_i || 0
|
|
order = %w{name user_count}.delete(params[:order])
|
|
dir = params[:asc] ? 'ASC' : 'DESC'
|
|
groups = Group.visible_groups(current_user, order ? "#{order} #{dir}" : nil)
|
|
|
|
if (filter = params[:filter]).present?
|
|
groups = Group.search_groups(filter, groups: groups)
|
|
end
|
|
|
|
type_filters = TYPE_FILTERS.keys
|
|
|
|
if username = params[:username]
|
|
groups = TYPE_FILTERS[:my].call(groups, User.find_by_username(username))
|
|
type_filters = type_filters - [:my, :owner]
|
|
end
|
|
|
|
unless guardian.is_staff?
|
|
# hide automatic groups from all non stuff to de-clutter page
|
|
groups = groups.where("automatic IS FALSE OR groups.id = #{Group::AUTO_GROUPS[:moderators]}")
|
|
type_filters.delete(:automatic)
|
|
end
|
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
end
|
|
|
|
if type = params[:type]&.to_sym
|
|
callback = TYPE_FILTERS[type]
|
|
if !callback
|
|
raise Discourse::InvalidParameters.new(:type)
|
|
end
|
|
groups = callback.call(groups, current_user)
|
|
end
|
|
|
|
if current_user
|
|
group_users = GroupUser.where(group: groups, user: current_user)
|
|
user_group_ids = group_users.pluck(:group_id)
|
|
owner_group_ids = group_users.where(owner: true).pluck(:group_id)
|
|
else
|
|
type_filters = type_filters - [:my, :owner]
|
|
end
|
|
|
|
count = groups.count
|
|
groups = groups.offset(page * page_size).limit(page_size)
|
|
|
|
render_json_dump(
|
|
groups: serialize_data(groups,
|
|
BasicGroupSerializer,
|
|
user_group_ids: user_group_ids || [],
|
|
owner_group_ids: owner_group_ids || []
|
|
),
|
|
extras: {
|
|
type_filters: type_filters
|
|
},
|
|
total_rows_groups: count,
|
|
load_more_groups: groups_path(
|
|
page: page + 1,
|
|
type: type,
|
|
order: order,
|
|
asc: params[:asc],
|
|
filter: filter
|
|
),
|
|
)
|
|
end
|
|
|
|
def show
|
|
respond_to do |format|
|
|
group = find_group(:id)
|
|
|
|
format.html do
|
|
@title = group.full_name.present? ? group.full_name.capitalize : group.name
|
|
@description_meta = group.bio_cooked.present? ? PrettyText.excerpt(group.bio_cooked, 300) : @title
|
|
render :show
|
|
end
|
|
|
|
format.json do
|
|
groups = Group.visible_groups(current_user)
|
|
|
|
if !guardian.is_staff?
|
|
groups = groups.where(automatic: false)
|
|
end
|
|
|
|
render_json_dump(
|
|
group: serialize_data(group, GroupShowSerializer, root: nil),
|
|
extras: {
|
|
visible_group_names: groups.pluck(:name)
|
|
}
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def new
|
|
end
|
|
|
|
def edit
|
|
end
|
|
|
|
def update
|
|
group = Group.find(params[:id])
|
|
guardian.ensure_can_edit!(group) unless current_user.admin
|
|
|
|
if group.update(group_params(automatic: group.automatic))
|
|
GroupActionLogger.new(current_user, group).log_change_group_settings
|
|
render json: success_json
|
|
else
|
|
render_json_error(group)
|
|
end
|
|
end
|
|
|
|
def posts
|
|
group = find_group(:group_id)
|
|
posts = group.posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(20)
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
end
|
|
|
|
def posts_feed
|
|
group = find_group(:group_id)
|
|
@posts = group.posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(50)
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_posts", group_name: group.name)}"
|
|
@link = Discourse.base_url
|
|
@description = I18n.t("rss_description.group_posts", group_name: group.name)
|
|
render 'posts/latest', formats: [:rss]
|
|
end
|
|
|
|
def mentions
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
|
group = find_group(:group_id)
|
|
posts = group.mentioned_posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(20)
|
|
render_serialized posts.to_a, GroupPostSerializer
|
|
end
|
|
|
|
def mentions_feed
|
|
raise Discourse::NotFound unless SiteSetting.enable_mentions?
|
|
group = find_group(:group_id)
|
|
@posts = group.mentioned_posts_for(
|
|
guardian,
|
|
params.permit(:before_post_id, :category_id)
|
|
).limit(50)
|
|
@title = "#{SiteSetting.title} - #{I18n.t("rss_description.group_mentions", group_name: group.name)}"
|
|
@link = Discourse.base_url
|
|
@description = I18n.t("rss_description.group_mentions", group_name: group.name)
|
|
render 'posts/latest', formats: [:rss]
|
|
end
|
|
|
|
def members
|
|
group = find_group(:group_id)
|
|
|
|
limit = (params[:limit] || 20).to_i
|
|
offset = params[:offset].to_i
|
|
|
|
if limit < 0
|
|
raise Discourse::InvalidParameters.new(:limit)
|
|
end
|
|
|
|
if offset < 0
|
|
raise Discourse::InvalidParameters.new(:offset)
|
|
end
|
|
|
|
dir = (params[:desc] && !params[:desc].blank?) ? 'DESC' : 'ASC'
|
|
order = ""
|
|
|
|
if params[:requesters]
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
users = group.requesters
|
|
total = users.count
|
|
|
|
if (filter = params[:filter]).present?
|
|
filter = filter.split(',') if filter.include?(',')
|
|
|
|
if current_user&.admin
|
|
users = users.filter_by_username_or_email(filter)
|
|
else
|
|
users = users.filter_by_username(filter)
|
|
end
|
|
end
|
|
|
|
users = users
|
|
.select("users.*, group_requests.reason, group_requests.created_at requested_at")
|
|
.order(params[:order] == 'requested_at' ? "group_requests.created_at #{dir}" : "")
|
|
.order(username_lower: dir)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
|
|
return render json: {
|
|
members: serialize_data(users, GroupRequesterSerializer),
|
|
meta: {
|
|
total: total,
|
|
limit: limit,
|
|
offset: offset
|
|
}
|
|
}
|
|
end
|
|
|
|
if params[:order] && %w{last_posted_at last_seen_at}.include?(params[:order])
|
|
order = "#{params[:order]} #{dir} NULLS LAST"
|
|
elsif params[:order] == 'added_at'
|
|
order = "group_users.created_at #{dir}"
|
|
end
|
|
|
|
users = group.users.human_users
|
|
total = users.count
|
|
|
|
if (filter = params[:filter]).present?
|
|
filter = filter.split(',') if filter.include?(',')
|
|
|
|
if current_user&.admin
|
|
users = users.filter_by_username_or_email(filter)
|
|
else
|
|
users = users.filter_by_username(filter)
|
|
end
|
|
end
|
|
|
|
users = users.select('users.*, group_users.created_at as added_at')
|
|
|
|
members = users
|
|
.order('NOT group_users.owner')
|
|
.order(order)
|
|
.order(username_lower: dir)
|
|
.limit(limit)
|
|
.offset(offset)
|
|
.includes(:primary_group)
|
|
|
|
owners = users
|
|
.order(order)
|
|
.order(username_lower: dir)
|
|
.where('group_users.owner')
|
|
.includes(:primary_group)
|
|
|
|
render json: {
|
|
members: serialize_data(members, GroupUserSerializer),
|
|
owners: serialize_data(owners, GroupUserSerializer),
|
|
meta: {
|
|
total: total,
|
|
limit: limit,
|
|
offset: offset
|
|
}
|
|
}
|
|
end
|
|
|
|
def add_members
|
|
group = Group.find(params[:id])
|
|
group.public_admission ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
|
|
|
users = users_from_params
|
|
|
|
if group.public_admission
|
|
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
|
|
unless current_user.staff?
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
end
|
|
end
|
|
|
|
if (usernames = group.users.where(id: users.pluck(:id)).pluck(:username)).present?
|
|
render_json_error(I18n.t(
|
|
"groups.errors.member_already_exist",
|
|
username: usernames.sort.join(", "),
|
|
count: usernames.size
|
|
))
|
|
else
|
|
users.each do |user|
|
|
group.add(user)
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
end
|
|
|
|
render json: success_json.merge!(
|
|
usernames: users.map(&:username)
|
|
)
|
|
end
|
|
end
|
|
|
|
def handle_membership_request
|
|
group = Group.find_by(id: params[:id])
|
|
raise Discourse::InvalidParameters.new(:id) if group.blank?
|
|
guardian.ensure_can_edit!(group)
|
|
|
|
ActiveRecord::Base.transaction do
|
|
user = User.find_by(id: params[:user_id])
|
|
raise Discourse::InvalidParameters.new(:user_id) if user.blank?
|
|
|
|
if params[:accept]
|
|
group.add(user)
|
|
GroupActionLogger.new(current_user, group).log_add_user_to_group(user)
|
|
end
|
|
|
|
GroupRequest.where(group_id: group.id, user_id: user.id).delete_all
|
|
end
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def mentionable
|
|
group = find_group(:group_id, ensure_can_see: false)
|
|
|
|
if group
|
|
render json: { mentionable: Group.mentionable(current_user).where(id: group.id).present? }
|
|
else
|
|
raise Discourse::InvalidAccess.new
|
|
end
|
|
end
|
|
|
|
def messageable
|
|
group = find_group(:group_id, ensure_can_see: false)
|
|
|
|
if group
|
|
render json: { messageable: Group.messageable(current_user).where(id: group.id).present? }
|
|
else
|
|
raise Discourse::InvalidAccess.new
|
|
end
|
|
end
|
|
|
|
def check_name
|
|
group_name = params.require(:group_name)
|
|
checker = UsernameCheckerService.new(allow_reserved_username: true)
|
|
render json: checker.check_username(group_name, nil)
|
|
end
|
|
|
|
def remove_member
|
|
group = Group.find_by(id: params[:id])
|
|
raise Discourse::NotFound unless group
|
|
group.public_exit ? ensure_logged_in : guardian.ensure_can_edit!(group)
|
|
|
|
# Maintain backwards compatibility
|
|
params[:usernames] = params[:username] if params[:username].present?
|
|
params[:user_emails] = params[:user_email] if params[:user_email].present?
|
|
|
|
users = users_from_params
|
|
|
|
if group.public_exit
|
|
if !guardian.can_log_group_changes?(group) && current_user != users.first
|
|
raise Discourse::InvalidAccess
|
|
end
|
|
|
|
unless current_user.staff?
|
|
RateLimiter.new(current_user, "public_group_membership", 3, 1.minute).performed!
|
|
end
|
|
end
|
|
|
|
users.each do |user|
|
|
group.remove(user)
|
|
GroupActionLogger.new(current_user, group).log_remove_user_from_group(user)
|
|
end
|
|
|
|
render json: success_json.merge!(
|
|
usernames: users.map(&:username)
|
|
)
|
|
|
|
end
|
|
|
|
def request_membership
|
|
params.require(:reason)
|
|
|
|
group = find_group(:id)
|
|
|
|
begin
|
|
GroupRequest.create!(group: group, user: current_user, reason: params[:reason])
|
|
rescue ActiveRecord::RecordNotUnique => e
|
|
return render json: failed_json.merge(error: I18n.t("groups.errors.already_requested_membership")), status: 409
|
|
end
|
|
|
|
usernames = [current_user.username].concat(
|
|
group.users.where('group_users.owner')
|
|
.order("users.last_seen_at DESC")
|
|
.limit(5)
|
|
.pluck("users.username")
|
|
)
|
|
|
|
raw = <<~EOF
|
|
#{params[:reason]}
|
|
|
|
---
|
|
<a href="#{Discourse.base_uri}/g/#{group.name}/requests">
|
|
#{I18n.t('groups.request_membership_pm.handle')}
|
|
</a>
|
|
EOF
|
|
|
|
post = PostCreator.new(current_user,
|
|
title: I18n.t('groups.request_membership_pm.title', group_name: group.name),
|
|
raw: raw,
|
|
archetype: Archetype.private_message,
|
|
target_usernames: usernames.join(','),
|
|
skip_validations: true
|
|
).create!
|
|
|
|
render json: success_json.merge(relative_url: post.topic.relative_url)
|
|
end
|
|
|
|
def set_notifications
|
|
group = find_group(:id)
|
|
notification_level = params.require(:notification_level)
|
|
|
|
user_id = current_user.id
|
|
if guardian.is_staff?
|
|
user_id = params[:user_id] || user_id
|
|
end
|
|
|
|
GroupUser.where(group_id: group.id)
|
|
.where(user_id: user_id)
|
|
.update_all(notification_level: notification_level)
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def histories
|
|
group = find_group(:group_id)
|
|
guardian.ensure_can_edit!(group) unless current_user.admin
|
|
|
|
page_size = 25
|
|
offset = (params[:offset] && params[:offset].to_i) || 0
|
|
|
|
group_histories = GroupHistory.with_filters(group, params[:filters])
|
|
.limit(page_size)
|
|
.offset(offset * page_size)
|
|
|
|
render_json_dump(
|
|
logs: serialize_data(group_histories, BasicGroupHistorySerializer),
|
|
all_loaded: group_histories.count < page_size
|
|
)
|
|
end
|
|
|
|
def search
|
|
groups = Group.visible_groups(current_user)
|
|
.where("groups.id <> ?", Group::AUTO_GROUPS[:everyone])
|
|
.order(:name)
|
|
|
|
if term = params[:term].to_s
|
|
groups = groups.where("name ILIKE :term OR full_name ILIKE :term", term: "%#{term}%")
|
|
end
|
|
|
|
if params[:ignore_automatic].to_s == "true"
|
|
groups = groups.where(automatic: false)
|
|
end
|
|
|
|
if Group.preloaded_custom_field_names.present?
|
|
Group.preload_custom_fields(groups, Group.preloaded_custom_field_names)
|
|
end
|
|
|
|
render_serialized(groups, BasicGroupSerializer)
|
|
end
|
|
|
|
private
|
|
|
|
def group_params(automatic: false)
|
|
permitted_params =
|
|
if automatic
|
|
%i{
|
|
visibility_level
|
|
mentionable_level
|
|
messageable_level
|
|
default_notification_level
|
|
bio_raw
|
|
}
|
|
else
|
|
default_params = %i{
|
|
mentionable_level
|
|
messageable_level
|
|
title
|
|
flair_url
|
|
flair_bg_color
|
|
flair_color
|
|
bio_raw
|
|
public_admission
|
|
public_exit
|
|
allow_membership_requests
|
|
full_name
|
|
default_notification_level
|
|
membership_request_template
|
|
}
|
|
|
|
if current_user.admin
|
|
default_params.push(*[
|
|
:incoming_email,
|
|
:primary_group,
|
|
:visibility_level,
|
|
:name,
|
|
:grant_trust_level,
|
|
:automatic_membership_email_domains,
|
|
:automatic_membership_retroactive
|
|
])
|
|
end
|
|
|
|
default_params
|
|
end
|
|
|
|
params.require(:group).permit(*permitted_params)
|
|
end
|
|
|
|
def find_group(param_name, ensure_can_see: true)
|
|
name = params.require(param_name)
|
|
group = Group
|
|
group = group.find_by("lower(name) = ?", name.downcase)
|
|
guardian.ensure_can_see!(group) if ensure_can_see
|
|
group
|
|
end
|
|
|
|
def users_from_params
|
|
if params[:usernames].present?
|
|
users = User.where(username_lower: params[:usernames].split(",").map(&:downcase))
|
|
raise Discourse::InvalidParameters.new(:usernames) if users.blank?
|
|
elsif params[:user_id].present?
|
|
users = User.where(id: params[:user_id].to_i)
|
|
raise Discourse::InvalidParameters.new(:user_id) if users.blank?
|
|
elsif params[:user_ids].present?
|
|
users = User.where(id: params[:user_ids].to_s.split(","))
|
|
raise Discourse::InvalidParameters.new(:user_ids) if users.blank?
|
|
elsif params[:user_emails].present?
|
|
users = User.with_email(params[:user_emails].split(","))
|
|
raise Discourse::InvalidParameters.new(:user_emails) if users.blank?
|
|
else
|
|
raise Discourse::InvalidParameters.new(
|
|
'user_ids or usernames or user_emails must be present'
|
|
)
|
|
end
|
|
users
|
|
end
|
|
end
|