mirror of
https://github.com/discourse/discourse.git
synced 2024-11-25 09:42:07 +08:00
FEATURE: Add site setting to show more detailed 404 errors. (#8014)
If the setting is turned on, then the user will receive information about the subject: if it was deleted or requires some special access to a group (only if the group is public). Otherwise, the user will receive a generic #404 error message. For now, this change affects only the topics and categories controller. This commit also tries to refactor some of the code related to error handling. To make error pages more consistent (design-wise), the actual error page will be rendered server-side.
This commit is contained in:
parent
d2bceff133
commit
fdb1d3404c
|
@ -945,46 +945,6 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
|||
}
|
||||
},
|
||||
|
||||
joinGroup() {
|
||||
const groupId = this.get("model.group.id");
|
||||
if (groupId) {
|
||||
if (this.get("model.group.allow_membership_requests")) {
|
||||
const groupName = this.get("model.group.name");
|
||||
return ajax(`/groups/${groupName}/request_membership`, {
|
||||
type: "POST",
|
||||
data: {
|
||||
topic_id: this.get("model.id")
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
bootbox.alert(
|
||||
I18n.t("topic.group_request_sent", {
|
||||
group_name: this.get("model.group.full_name")
|
||||
}),
|
||||
() =>
|
||||
this.previousURL
|
||||
? DiscourseURL.routeTo(this.previousURL)
|
||||
: DiscourseURL.routeTo("/")
|
||||
);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
} else {
|
||||
const topic = this.model;
|
||||
return ajax(`/groups/${groupId}/members`, {
|
||||
type: "PUT",
|
||||
data: { user_id: this.get("currentUser.id") }
|
||||
})
|
||||
.then(() =>
|
||||
topic.reload().then(() => {
|
||||
topic.set("view_hidden", false);
|
||||
topic.postStream.refresh();
|
||||
})
|
||||
)
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
replyAsNewTopic(post, quotedText) {
|
||||
const composerController = this.composer;
|
||||
|
||||
|
@ -1182,7 +1142,7 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
|||
}
|
||||
},
|
||||
|
||||
hasError: Ember.computed.or("model.notFoundHtml", "model.message"),
|
||||
hasError: Ember.computed.or("model.errorHtml", "model.errorMessage"),
|
||||
noErrorYet: Ember.computed.not("hasError"),
|
||||
|
||||
categories: Ember.computed.alias("site.categoriesList"),
|
||||
|
|
|
@ -1067,31 +1067,16 @@ export default RestModel.extend({
|
|||
// Handles an error loading a topic based on a HTTP status code. Updates
|
||||
// the text to the correct values.
|
||||
errorLoading(result) {
|
||||
const status = result.jqXHR.status;
|
||||
|
||||
const topic = this.topic;
|
||||
this.set("loadingFilter", false);
|
||||
topic.set("errorLoading", true);
|
||||
|
||||
// If the result was 404 the post is not found
|
||||
// If it was 410 the post is deleted and the user should not see it
|
||||
if (status === 404 || status === 410) {
|
||||
topic.set("notFoundHtml", result.jqXHR.responseText);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the result is 403 it means invalid access
|
||||
if (status === 403) {
|
||||
topic.set("noRetry", true);
|
||||
if (Discourse.User.current()) {
|
||||
topic.set("message", I18n.t("topic.invalid_access.description"));
|
||||
const json = result.jqXHR.responseJSON;
|
||||
if (json && json.extras && json.extras.html) {
|
||||
topic.set("errorHtml", json.extras.html);
|
||||
} else {
|
||||
topic.set("message", I18n.t("topic.invalid_access.login_required"));
|
||||
topic.set("errorMessage", I18n.t("topic.server_error.description"));
|
||||
topic.set("noRetry", result.jqXHR.status === 403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise supply a generic error message
|
||||
topic.set("message", I18n.t("topic.server_error.description"));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,16 +17,15 @@ import {
|
|||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export function loadTopicView(topic, args) {
|
||||
const topicId = topic.get("id");
|
||||
const data = _.merge({}, args);
|
||||
const url = `${Discourse.getURL("/t/")}${topicId}`;
|
||||
const url = `${Discourse.getURL("/t/")}${topic.id}`;
|
||||
const jsonUrl = (data.nearPost ? `${url}/${data.nearPost}` : url) + ".json";
|
||||
|
||||
delete data.nearPost;
|
||||
delete data.__type;
|
||||
delete data.store;
|
||||
|
||||
return PreloadStore.getAndRemove(`topic_${topicId}`, () =>
|
||||
return PreloadStore.getAndRemove(`topic_${topic.id}`, () =>
|
||||
ajax(jsonUrl, { data })
|
||||
).then(json => {
|
||||
topic.updateFromJson(json);
|
||||
|
|
|
@ -198,6 +198,18 @@ export default (filterArg, params) => {
|
|||
},
|
||||
|
||||
actions: {
|
||||
error(err) {
|
||||
const json = err.jqXHR.responseJSON;
|
||||
if (json && json.extras && json.extras.html) {
|
||||
this.controllerFor("discovery").set(
|
||||
"errorHtml",
|
||||
err.jqXHR.responseJSON.extras.html
|
||||
);
|
||||
} else {
|
||||
this.replaceWith("exception");
|
||||
}
|
||||
},
|
||||
|
||||
setNotification(notification_level) {
|
||||
this.currentModel.setNotification(notification_level);
|
||||
},
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
{{#if errorHtml}}
|
||||
{{{errorHtml}}}
|
||||
{{else}}
|
||||
<div class="container">
|
||||
{{discourse-banner user=currentUser banner=site.banner}}
|
||||
</div>
|
||||
|
@ -30,3 +33,4 @@
|
|||
</div>
|
||||
|
||||
{{plugin-outlet name="discovery-below"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
{{#discourse-topic multiSelect=multiSelect enteredAt=enteredAt topic=model hasScrolled=hasScrolled}}
|
||||
{{#if model.view_hidden}}
|
||||
{{topic-join-group-notice model=model action=(action "joinGroup")}}
|
||||
{{else}}
|
||||
{{#if model}}
|
||||
{{add-category-tag-classes category=model.category tags=model.tags}}
|
||||
<div class="container">
|
||||
|
@ -349,11 +346,11 @@
|
|||
{{else}}
|
||||
<div class="container">
|
||||
{{#conditional-loading-spinner condition=noErrorYet}}
|
||||
{{#if model.notFoundHtml}}
|
||||
<div class="not-found">{{{model.notFoundHtml}}}</div>
|
||||
{{#if model.errorHtml}}
|
||||
<div class="not-found">{{{model.errorHtml}}}</div>
|
||||
{{else}}
|
||||
<div class="topic-error">
|
||||
<div>{{model.message}}</div>
|
||||
<div>{{model.errorMessage}}</div>
|
||||
{{#if model.noRetry}}
|
||||
{{#unless currentUser}}
|
||||
{{d-button action=(route-action "showLogin") class="btn-primary topic-retry" icon="user" label="log_in"}}
|
||||
|
@ -373,5 +370,4 @@
|
|||
{{#if embedQuoteButton}}
|
||||
{{quote-button quoteState=quoteState selectText=(action "selectText")}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/discourse-topic}}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// Page not found styles
|
||||
|
||||
h1.page-not-found {
|
||||
.page-not-found {
|
||||
margin: 0 0 40px 0;
|
||||
|
||||
h1.title {
|
||||
font-size: $font-up-5;
|
||||
line-height: $line-height-medium;
|
||||
}
|
||||
|
||||
.page-not-found {
|
||||
margin: 0 0 40px 0;
|
||||
&-search {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
|
@ -98,15 +98,59 @@ class ApplicationController < ActionController::Base
|
|||
use_crawler_layout? ? 'crawler' : 'application'
|
||||
end
|
||||
|
||||
# Some exceptions
|
||||
class RenderEmpty < StandardError; end
|
||||
class PluginDisabled < StandardError; end
|
||||
|
||||
# Render nothing
|
||||
rescue_from RenderEmpty do
|
||||
render 'default/empty'
|
||||
end
|
||||
|
||||
def render_rate_limit_error(e)
|
||||
rescue_from ArgumentError do |e|
|
||||
if e.message == "string contains null byte"
|
||||
raise Discourse::InvalidParameters, e.message
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from PG::ReadOnlySqlTransaction do |e|
|
||||
Discourse.received_postgres_readonly!
|
||||
Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}")
|
||||
raise Discourse::ReadOnly
|
||||
end
|
||||
|
||||
rescue_from ActionController::ParameterMissing do |e|
|
||||
render_json_error e.message, status: 400
|
||||
end
|
||||
|
||||
rescue_from ActionController::RoutingError, PluginDisabled do
|
||||
rescue_discourse_actions(:not_found, 404)
|
||||
end
|
||||
|
||||
# Handles requests for giant IDs that throw pg exceptions
|
||||
rescue_from ActiveModel::RangeError do |e|
|
||||
if e.message =~ /ActiveModel::Type::Integer/
|
||||
rescue_discourse_actions(:not_found, 404)
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid do |e|
|
||||
if request.format && request.format.json?
|
||||
render_json_error e, type: :record_invalid, status: 422
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::StatementInvalid do |e|
|
||||
Discourse.reset_active_record_cache_if_needed(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
# If they hit the rate limiter
|
||||
rescue_from RateLimiter::LimitExceeded do |e|
|
||||
retry_time_in_seconds = e&.available_in
|
||||
|
||||
render_json_error(
|
||||
|
@ -118,25 +162,6 @@ class ApplicationController < ActionController::Base
|
|||
)
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid do |e|
|
||||
if request.format && request.format.json?
|
||||
render_json_error e, type: :record_invalid, status: 422
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
# If they hit the rate limiter
|
||||
rescue_from RateLimiter::LimitExceeded do |e|
|
||||
render_rate_limit_error(e)
|
||||
end
|
||||
|
||||
rescue_from PG::ReadOnlySqlTransaction do |e|
|
||||
Discourse.received_postgres_readonly!
|
||||
Rails.logger.error("#{e.class} #{e.message}: #{e.backtrace.join("\n")}")
|
||||
raise Discourse::ReadOnly
|
||||
end
|
||||
|
||||
rescue_from Discourse::NotLoggedIn do |e|
|
||||
if (request.format && request.format.json?) || request.xhr? || !request.get?
|
||||
rescue_discourse_actions(:not_logged_in, 403, include_ember: true)
|
||||
|
@ -145,14 +170,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
if e.message == "string contains null byte"
|
||||
raise Discourse::InvalidParameters, e.message
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from Discourse::InvalidParameters do |e|
|
||||
message = I18n.t('invalid_params', message: e.message)
|
||||
if (request.format && request.format.json?) || request.xhr? || !request.get?
|
||||
|
@ -162,45 +179,27 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::StatementInvalid do |e|
|
||||
Discourse.reset_active_record_cache_if_needed(e)
|
||||
raise e
|
||||
end
|
||||
|
||||
class PluginDisabled < StandardError; end
|
||||
|
||||
# Handles requests for giant IDs that throw pg exceptions
|
||||
rescue_from ActiveModel::RangeError do |e|
|
||||
if e.message =~ /ActiveModel::Type::Integer/
|
||||
rescue_discourse_actions(:not_found, 404)
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from Discourse::NotFound do |e|
|
||||
rescue_discourse_actions(
|
||||
:not_found,
|
||||
e.status,
|
||||
check_permalinks: e.check_permalinks,
|
||||
original_path: e.original_path
|
||||
original_path: e.original_path,
|
||||
custom_message: e.custom_message
|
||||
)
|
||||
end
|
||||
|
||||
rescue_from PluginDisabled, ActionController::RoutingError do
|
||||
rescue_discourse_actions(:not_found, 404)
|
||||
end
|
||||
|
||||
rescue_from Discourse::InvalidAccess do |e|
|
||||
|
||||
if e.opts[:delete_cookie].present?
|
||||
cookies.delete(e.opts[:delete_cookie])
|
||||
end
|
||||
|
||||
rescue_discourse_actions(
|
||||
:invalid_access,
|
||||
403,
|
||||
include_ember: true,
|
||||
custom_message: e.custom_message
|
||||
custom_message: e.custom_message,
|
||||
group: e.group
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -208,10 +207,6 @@ class ApplicationController < ActionController::Base
|
|||
render_json_error I18n.t('read_only_mode_enabled'), type: :read_only, status: 503
|
||||
end
|
||||
|
||||
rescue_from ActionController::ParameterMissing do |e|
|
||||
render_json_error e.message, status: 400
|
||||
end
|
||||
|
||||
def redirect_with_client_support(url, options)
|
||||
if request.xhr?
|
||||
response.headers['Discourse-Xhr-Redirect'] = 'true'
|
||||
|
@ -241,18 +236,21 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
message = opts[:custom_message_translated] || I18n.t(opts[:custom_message] || type)
|
||||
error_page_opts = {
|
||||
title: opts[:custom_message_translated] || I18n.t(opts[:custom_message] || "page_not_found.title"),
|
||||
status: status_code,
|
||||
group: opts[:group]
|
||||
}
|
||||
|
||||
if show_json_errors
|
||||
# HACK: do not use render_json_error for topics#show
|
||||
if request.params[:controller] == 'topics' && request.params[:action] == 'show'
|
||||
return render(
|
||||
status: status_code,
|
||||
layout: false,
|
||||
plain: (status_code == 404 || status_code == 410) ? build_not_found_page(status_code) : message
|
||||
)
|
||||
opts = { type: type, status: status_code }
|
||||
|
||||
# Include error in HTML format for topics#show.
|
||||
if (request.params[:controller] == 'topics' && request.params[:action] == 'show') || (request.params[:controller] == 'categories' && request.params[:action] == 'find_by_slug')
|
||||
opts[:extras] = { html: build_not_found_page(error_page_opts) }
|
||||
end
|
||||
|
||||
render_json_error message, type: type, status: status_code
|
||||
render_json_error message, opts
|
||||
else
|
||||
begin
|
||||
# 404 pages won't have the session and theme_keys without these:
|
||||
|
@ -262,7 +260,8 @@ class ApplicationController < ActionController::Base
|
|||
return render plain: message, status: status_code
|
||||
end
|
||||
|
||||
render html: build_not_found_page(status_code, opts[:include_ember] ? 'application' : 'no_ember')
|
||||
error_page_opts[:layout] = opts[:include_ember] ? 'application' : 'no_ember'
|
||||
render html: build_not_found_page(error_page_opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -754,13 +753,13 @@ class ApplicationController < ActionController::Base
|
|||
raise Discourse::ReadOnly.new if !(request.get? || request.head?) && @readonly_mode
|
||||
end
|
||||
|
||||
def build_not_found_page(status = 404, layout = false)
|
||||
def build_not_found_page(opts = {})
|
||||
if SiteSetting.bootstrap_error_pages?
|
||||
preload_json
|
||||
layout = 'application' if layout == 'no_ember'
|
||||
opts[:layout] = 'application' if opts[:layout] == 'no_ember'
|
||||
end
|
||||
|
||||
if !SiteSetting.login_required? || current_user
|
||||
if !SiteSetting.login_required? || (current_user rescue false)
|
||||
key = "page_not_found_topics"
|
||||
if @topics_partial = $redis.get(key)
|
||||
@topics_partial = @topics_partial.html_safe
|
||||
|
@ -774,9 +773,12 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
@container_class = "wrap not-found-container"
|
||||
@slug = (params[:slug].presence || params[:id].presence || "").tr('-', '')
|
||||
@title = opts[:title] || I18n.t("page_not_found.title")
|
||||
@group = opts[:group]
|
||||
@hide_search = true if SiteSetting.login_required
|
||||
render_to_string status: status, layout: layout, formats: [:html], template: '/exceptions/not_found'
|
||||
@slug = (params[:slug].presence || params[:id].presence || "").tr('-', ' ')
|
||||
|
||||
render_to_string status: opts[:status], layout: opts[:layout], formats: [:html], template: '/exceptions/not_found'
|
||||
end
|
||||
|
||||
def is_asset_path
|
||||
|
|
|
@ -206,7 +206,18 @@ class CategoriesController < ApplicationController
|
|||
def find_by_slug
|
||||
params.require(:category_slug)
|
||||
@category = Category.find_by_slug(params[:category_slug], params[:parent_category_slug])
|
||||
guardian.ensure_can_see!(@category)
|
||||
if !guardian.can_see?(@category)
|
||||
if SiteSetting.detailed_404 && group = @category.access_category_via_group
|
||||
raise Discourse::InvalidAccess.new(
|
||||
'not in group',
|
||||
@category,
|
||||
custom_message: 'not_in_group.title_category',
|
||||
group: group
|
||||
)
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
end
|
||||
|
||||
@category.permission = CategoryGroup.permission_types[:full] if Category.topic_create_allowed(guardian).where(id: @category.id).exists?
|
||||
render_serialized(@category, CategorySerializer)
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
class ExceptionsController < ApplicationController
|
||||
skip_before_action :check_xhr, :preload_json
|
||||
before_action :hide_search
|
||||
|
||||
def not_found
|
||||
# centralize all rendering of 404 into app controller
|
||||
|
@ -11,13 +10,7 @@ class ExceptionsController < ApplicationController
|
|||
|
||||
# Give us an endpoint to use for 404 content in the ember app
|
||||
def not_found_body
|
||||
render html: build_not_found_page(200, false)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def hide_search
|
||||
@hide_search = true if SiteSetting.login_required
|
||||
render html: build_not_found_page(status: 200)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -368,7 +368,13 @@ class ListController < ApplicationController
|
|||
raise Discourse::NotFound.new("category not found", check_permalinks: true) if !@category
|
||||
|
||||
@description_meta = @category.description_text
|
||||
raise Discourse::NotFound unless guardian.can_see?(@category)
|
||||
if !guardian.can_see?(@category)
|
||||
if SiteSetting.detailed_404
|
||||
raise Discourse::InvalidAccess
|
||||
else
|
||||
raise Discourse::NotFound
|
||||
end
|
||||
end
|
||||
|
||||
if use_crawler_layout?
|
||||
@subcategories = @category.subcategories.select { |c| guardian.can_see?(c) }
|
||||
|
|
|
@ -79,12 +79,54 @@ class TopicsController < ApplicationController
|
|||
|
||||
begin
|
||||
@topic_view = TopicView.new(params[:id] || params[:topic_id], current_user, opts)
|
||||
rescue Discourse::NotFound
|
||||
rescue Discourse::NotFound => ex
|
||||
if params[:id]
|
||||
topic = Topic.find_by(slug: params[:id].downcase)
|
||||
return redirect_to_correct_topic(topic, opts[:post_number]) if topic
|
||||
end
|
||||
|
||||
raise ex
|
||||
rescue Discourse::NotLoggedIn => ex
|
||||
if !SiteSetting.detailed_404
|
||||
raise Discourse::NotFound
|
||||
else
|
||||
raise ex
|
||||
end
|
||||
rescue Discourse::InvalidAccess => ex
|
||||
# If the user can't see the topic, clean up notifications for it.
|
||||
Notification.remove_for(current_user.id, params[:topic_id]) if current_user
|
||||
|
||||
deleted = guardian.can_see_topic?(ex.obj, false) ||
|
||||
(!guardian.can_see_topic?(ex.obj) &&
|
||||
ex.obj&.access_topic_via_group &&
|
||||
ex.obj.deleted_at)
|
||||
|
||||
if SiteSetting.detailed_404
|
||||
if deleted
|
||||
raise Discourse::NotFound.new(
|
||||
'deleted topic',
|
||||
custom_message: 'deleted_topic',
|
||||
status: 410,
|
||||
check_permalinks: true,
|
||||
original_path: ex.obj.relative_url
|
||||
)
|
||||
elsif !guardian.can_see_topic?(ex.obj) && group = ex.obj&.access_topic_via_group
|
||||
raise Discourse::InvalidAccess.new(
|
||||
'not in group',
|
||||
ex.obj,
|
||||
custom_message: 'not_in_group.title_topic',
|
||||
group: group
|
||||
)
|
||||
end
|
||||
|
||||
raise ex
|
||||
else
|
||||
raise Discourse::NotFound.new(
|
||||
nil,
|
||||
check_permalinks: deleted,
|
||||
original_path: ex.obj.relative_url
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
page = params[:page]
|
||||
|
@ -120,27 +162,6 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
|
||||
perform_show_response
|
||||
|
||||
rescue Discourse::InvalidAccess => ex
|
||||
if !guardian.can_see_topic?(ex.obj) && guardian.can_get_access_to_topic?(ex.obj)
|
||||
return perform_hidden_topic_show_response(ex.obj)
|
||||
end
|
||||
|
||||
if current_user
|
||||
# If the user can't see the topic, clean up notifications for it.
|
||||
Notification.remove_for(current_user.id, params[:topic_id])
|
||||
end
|
||||
|
||||
if ex.obj && Topic === ex.obj && guardian.can_see_topic_if_not_deleted?(ex.obj)
|
||||
raise Discourse::NotFound.new(
|
||||
"topic was deleted",
|
||||
status: 410,
|
||||
check_permalinks: true,
|
||||
original_path: ex.obj.relative_url
|
||||
)
|
||||
end
|
||||
|
||||
raise ex
|
||||
end
|
||||
|
||||
def publish
|
||||
|
@ -960,19 +981,6 @@ class TopicsController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def perform_hidden_topic_show_response(topic)
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
@topic_view = nil
|
||||
render :show
|
||||
end
|
||||
|
||||
format.json do
|
||||
render_serialized(topic, HiddenTopicViewSerializer, root: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def render_topic_changes(dest_topic)
|
||||
if dest_topic.present?
|
||||
render json: { success: true, url: dest_topic.relative_url }
|
||||
|
|
|
@ -606,8 +606,6 @@ class UsersController < ApplicationController
|
|||
end
|
||||
end
|
||||
end
|
||||
rescue RateLimiter::LimitExceeded => e
|
||||
render_rate_limit_error(e)
|
||||
rescue ::Webauthn::SecurityKeyError => err
|
||||
render json: {
|
||||
message: err.message,
|
||||
|
|
|
@ -264,6 +264,15 @@ class Category < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
def access_category_via_group
|
||||
Group
|
||||
.joins(:category_groups)
|
||||
.where("category_groups.category_id = ?", self.id)
|
||||
.where("groups.public_admission OR groups.allow_membership_requests")
|
||||
.order(:allow_membership_requests)
|
||||
.first
|
||||
end
|
||||
|
||||
def duplicate_slug?
|
||||
Category.where(slug: self.slug, parent_category_id: parent_category_id).where.not(id: id).any?
|
||||
end
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HiddenTopicViewSerializer < ApplicationSerializer
|
||||
attributes :view_hidden?
|
||||
|
||||
has_one :group, serializer: BasicGroupSerializer, root: false, embed: :objects
|
||||
|
||||
def view_hidden?
|
||||
true
|
||||
end
|
||||
|
||||
def group
|
||||
object.access_topic_via_group
|
||||
end
|
||||
end
|
|
@ -1,4 +1,12 @@
|
|||
<h1 class="page-not-found"><%= t 'page_not_found.title' %></h1>
|
||||
<div class="page-not-found">
|
||||
<h1 class="title"><%= @title %></h1>
|
||||
|
||||
<%- if @group&.allow_membership_requests %>
|
||||
<a href="<%= group_path @group.name %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.request_membership') %></a>
|
||||
<%- elsif @group&.public_admission %>
|
||||
<a href="<%= group_path @group.name %>" class='btn btn-primary'><%= SvgSprite.raw_svg('user-plus') %> <%= I18n.t('not_in_group.join_group') %></a>
|
||||
<%- end %>
|
||||
</div>
|
||||
|
||||
<%= build_plugin_html 'server:not-found-before-topics' %>
|
||||
|
||||
|
|
|
@ -252,6 +252,12 @@ en:
|
|||
invalid_grant_badge_reason_link: "External or invalid discourse link is not allowed in badge reason"
|
||||
email_template_cant_be_modified: "This email template can't be modified"
|
||||
invalid_whisper_access: "Either whispers are not enabled or you do not have access to create whisper posts"
|
||||
not_in_group:
|
||||
title_topic: "You must be in a group to see this topic."
|
||||
title_category: "You must be in a group to see this category."
|
||||
request_membership: "Request Membership"
|
||||
join_group: "Join Group"
|
||||
deleted_topic: "Oops! This topic has been deleted and is no longer available."
|
||||
|
||||
reading_time: "Reading time"
|
||||
likes: "Likes"
|
||||
|
@ -1412,6 +1418,7 @@ en:
|
|||
notification_email: "The from: email address used when sending all essential system emails. The domain specified here must have SPF, DKIM and reverse PTR records set correctly for email to arrive."
|
||||
email_custom_headers: "A pipe-delimited list of custom email headers"
|
||||
email_subject: "Customizable subject format for standard emails. See <a href='https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801' target='_blank'>https://meta.discourse.org/t/customize-subject-format-for-standard-emails/20801</a>"
|
||||
detailed_404: "Provides more details to users about why they can’t access a particular topic. Note: This is less secure because users will know if a URL links to a valid topic."
|
||||
enforce_second_factor: "Forces users to enable second factor authentication. Select 'all' to enforce it to all users. Select 'staff' to enforce it to staff users only."
|
||||
force_https: "Force your site to use HTTPS only. WARNING: do NOT enable this until you verify HTTPS is fully set up and working absolutely everywhere! Did you check your CDN, all social logins, and any external logos / dependencies to make sure they are all HTTPS compatible, too?"
|
||||
same_site_cookies: "Use same site cookies, they eliminate all vectors Cross Site Request Forgery on supported browsers (Lax or Strict). Warning: Strict will only work on sites that force login and use SSO."
|
||||
|
|
|
@ -1315,6 +1315,7 @@ trust:
|
|||
default: true
|
||||
|
||||
security:
|
||||
detailed_404: false
|
||||
enforce_second_factor:
|
||||
client: true
|
||||
type: enum
|
||||
|
|
|
@ -71,13 +71,18 @@ module Discourse
|
|||
|
||||
# When they don't have permission to do something
|
||||
class InvalidAccess < StandardError
|
||||
attr_reader :obj, :custom_message, :opts
|
||||
attr_reader :obj
|
||||
attr_reader :opts
|
||||
attr_reader :custom_message
|
||||
attr_reader :group
|
||||
|
||||
def initialize(msg = nil, obj = nil, opts = nil)
|
||||
super(msg)
|
||||
|
||||
@opts = opts || {}
|
||||
@custom_message = opts[:custom_message] if @opts[:custom_message]
|
||||
@obj = obj
|
||||
@custom_message = opts[:custom_message] if @opts[:custom_message]
|
||||
@group = opts[:group] if @opts[:group]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -86,12 +91,15 @@ module Discourse
|
|||
attr_reader :status
|
||||
attr_reader :check_permalinks
|
||||
attr_reader :original_path
|
||||
attr_reader :custom_message
|
||||
|
||||
def initialize(msg = nil, status: 404, check_permalinks: false, original_path: nil, custom_message: nil)
|
||||
super(msg)
|
||||
|
||||
def initialize(message = nil, status: 404, check_permalinks: false, original_path: nil)
|
||||
@status = status
|
||||
@check_permalinks = check_permalinks
|
||||
@original_path = original_path
|
||||
super(message)
|
||||
@custom_message = custom_message
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -155,10 +155,6 @@ module TopicGuardian
|
|||
can_see_category?(topic.category)
|
||||
end
|
||||
|
||||
def can_see_topic_if_not_deleted?(topic)
|
||||
can_see_topic?(topic, false)
|
||||
end
|
||||
|
||||
def can_get_access_to_topic?(topic)
|
||||
topic&.access_topic_via_group.present? && authenticated?
|
||||
end
|
||||
|
|
|
@ -145,6 +145,12 @@ RSpec.describe ApplicationController do
|
|||
topic = create_post.topic
|
||||
Permalink.create!(url: topic.relative_url, topic_id: topic.id + 1)
|
||||
topic.trash!
|
||||
|
||||
SiteSetting.detailed_404 = false
|
||||
get topic.relative_url
|
||||
expect(response.status).to eq(404)
|
||||
|
||||
SiteSetting.detailed_404 = true
|
||||
get topic.relative_url
|
||||
expect(response.status).to eq(410)
|
||||
end
|
||||
|
|
|
@ -1229,12 +1229,13 @@ RSpec.describe TopicsController do
|
|||
|
||||
describe 'when topic is not allowed' do
|
||||
it 'should return the right response' do
|
||||
SiteSetting.detailed_404 = true
|
||||
sign_in(user)
|
||||
|
||||
get "/t/#{private_topic.id}.json"
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
expect(response.body).to eq(I18n.t('invalid_access'))
|
||||
expect(response.body).to include(I18n.t('invalid_access'))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1379,6 +1380,120 @@ RSpec.describe TopicsController do
|
|||
end
|
||||
end
|
||||
|
||||
context 'without detailed error pages' do
|
||||
before do
|
||||
SiteSetting.detailed_404 = false
|
||||
end
|
||||
|
||||
context 'anonymous' do
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
secure_topic: 404,
|
||||
private_topic: 404,
|
||||
deleted_topic: 404,
|
||||
deleted_secure_topic: 404,
|
||||
deleted_private_topic: 404,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 404
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
||||
context 'anonymous with login required' do
|
||||
before do
|
||||
SiteSetting.login_required = true
|
||||
end
|
||||
expected = {
|
||||
normal_topic: 302,
|
||||
secure_topic: 302,
|
||||
private_topic: 302,
|
||||
deleted_topic: 302,
|
||||
deleted_secure_topic: 302,
|
||||
deleted_private_topic: 302,
|
||||
nonexist: 302,
|
||||
secure_accessible_topic: 302
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
||||
context 'normal user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
secure_topic: 404,
|
||||
private_topic: 404,
|
||||
deleted_topic: 404,
|
||||
deleted_secure_topic: 404,
|
||||
deleted_private_topic: 404,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 404
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
||||
context 'allowed user' do
|
||||
before do
|
||||
sign_in(allowed_user)
|
||||
end
|
||||
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
secure_topic: 200,
|
||||
private_topic: 200,
|
||||
deleted_topic: 404,
|
||||
deleted_secure_topic: 404,
|
||||
deleted_private_topic: 404,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 404
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
||||
context 'moderator' do
|
||||
before do
|
||||
sign_in(moderator)
|
||||
end
|
||||
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
secure_topic: 404,
|
||||
private_topic: 404,
|
||||
deleted_topic: 200,
|
||||
deleted_secure_topic: 404,
|
||||
deleted_private_topic: 404,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 404
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
||||
context 'admin' do
|
||||
before do
|
||||
sign_in(admin)
|
||||
end
|
||||
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
secure_topic: 200,
|
||||
private_topic: 200,
|
||||
deleted_topic: 200,
|
||||
deleted_secure_topic: 200,
|
||||
deleted_private_topic: 200,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 200
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
end
|
||||
|
||||
context 'with detailed error pages' do
|
||||
before do
|
||||
SiteSetting.detailed_404 = true
|
||||
end
|
||||
|
||||
context 'anonymous' do
|
||||
expected = {
|
||||
normal_topic: 200,
|
||||
|
@ -1423,7 +1538,7 @@ RSpec.describe TopicsController do
|
|||
deleted_secure_topic: 403,
|
||||
deleted_private_topic: 403,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 200
|
||||
secure_accessible_topic: 403
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
@ -1441,7 +1556,7 @@ RSpec.describe TopicsController do
|
|||
deleted_secure_topic: 410,
|
||||
deleted_private_topic: 410,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 200
|
||||
secure_accessible_topic: 403
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
@ -1459,7 +1574,7 @@ RSpec.describe TopicsController do
|
|||
deleted_secure_topic: 403,
|
||||
deleted_private_topic: 403,
|
||||
nonexist: 404,
|
||||
secure_accessible_topic: 200
|
||||
secure_accessible_topic: 403
|
||||
}
|
||||
include_examples "various scenarios", expected
|
||||
end
|
||||
|
@ -1483,6 +1598,8 @@ RSpec.describe TopicsController do
|
|||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
it 'records a view' do
|
||||
expect do
|
||||
get "/t/#{topic.slug}/#{topic.id}.json"
|
||||
|
@ -1637,7 +1754,7 @@ RSpec.describe TopicsController do
|
|||
[:json, :html].each do |format|
|
||||
get "/t/#{topic.slug}/#{topic.id}.#{format}", params: { api_key: "bad" }
|
||||
|
||||
expect(response.code.to_i).to be(403)
|
||||
expect(response.code.to_i).to eq(403)
|
||||
expect(response.body).to include(I18n.t("invalid_access"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -19,10 +19,7 @@ QUnit.test("Enter without an id", async assert => {
|
|||
QUnit.test("Enter a 404 topic", async assert => {
|
||||
await visit("/t/not-found/404");
|
||||
assert.ok(!exists("#topic"), "The topic was not rendered");
|
||||
assert.ok(
|
||||
find(".not-found").text() === "not found",
|
||||
"it renders the error message"
|
||||
);
|
||||
assert.ok(exists(".topic-error"), "An error message is displayed");
|
||||
});
|
||||
|
||||
QUnit.test("Enter without access", async assert => {
|
||||
|
|
Loading…
Reference in New Issue
Block a user