core: strict discussion visiblility check (#537)

This commit is contained in:
panda authored and GitHub committed 2023-03-12 20:11:11 +08:00
1 parent f39a28bfec
commit b80c80d9b2
4 files changed
+44 -18

No files matched your search

+14 -14
View File
@@ -1,7 +1,7 @@
import { isSafeInteger } from 'lodash';
import { ObjectId } from 'mongodb';
import {
DiscussionLockedError, DiscussionNotFoundError, DocumentNotFoundError,
DiscussionLockedError, DiscussionNodeNotFoundError, DiscussionNotFoundError, DocumentNotFoundError,
PermissionError,
} from '../error';
import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface';
@@ -30,7 +30,7 @@ class DiscussionHandler extends Handler {
vnode?: any;
@param('type', Types.Range(Object.keys(typeMapper)), true)
@param('name', Types.Name, true)
@param('name', Types.String, true)
@param('did', Types.ObjectId, true)
@param('drid', Types.ObjectId, true)
@param('drrid', Types.ObjectId, true)
@@ -55,15 +55,10 @@ class DiscussionHandler extends Handler {
}
}
}
// TODO(twd2): do more visibility check eg. contest
// TODO(twd2): exclude problem/contest discussions?
// TODO(iceboy): continuation based pagination.
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name, this.user._id);
if (this.vnode.assign?.length && !this.user.own(this.vnode)) {
if (!Set.intersection(this.vnode.assign, this.user.group).size) {
throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
}
}
if (!discussion.checkVNodeVisibility(typeMapper[type], this.vnode, this.user)) throw new DiscussionNodeNotFoundError(this.vnode.id);
if (this.ddoc) {
this.ddoc.parentType ||= this.vnode.type;
this.ddoc.parentId ||= this.vnode.id;
@@ -73,11 +68,13 @@ class DiscussionHandler extends Handler {
class DiscussionMainHandler extends Handler {
@param('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) {
@param('all', Types.Boolean)
async get(domainId: string, page = 1, all = false) {
// Limit to known types
const parentType = { $in: Object.keys(typeMapper).map((i) => typeMapper[i]) };
all &&= this.user.hasPerm(PERM.PERM_MOD_BADGE);
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId, { parentType }),
discussion.getMulti(domainId, { parentType, ...all ? {} : { hidden: false } }),
page,
system.get('pagination.discussion'),
);
@@ -95,15 +92,16 @@ class DiscussionMainHandler extends Handler {
class DiscussionNodeHandler extends DiscussionHandler {
@param('type', Types.Range(Object.keys(typeMapper)))
@param('name', Types.Name)
@param('name', Types.String)
@param('page', Types.PositiveInt, true)
async get(domainId: string, type: string, _name: string, page = 1) {
let name: ObjectId | string | number;
if (ObjectId.isValid(_name)) name = new ObjectId(_name);
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
else name = _name;
const hidden = this.user.own(this.vnode) || this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION) ? {} : { hidden: false };
const [ddocs, dpcount] = await paginate(
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name, ...hidden }),
page,
system.get('pagination.discussion'),
);
@@ -152,9 +150,10 @@ class DiscussionCreateHandler extends DiscussionHandler {
await this.limitRate('add_discussion', 3600, 60);
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
const hidden = this.vnode.hidden ?? false;
const did = await discussion.add(
domainId, typeMapper[type], this.vnode.id, this.user._id,
title, content, this.request.ip, highlight, pin,
title, content, this.request.ip, highlight, pin, hidden,
);
this.response.body = { did };
this.response.redirect = this.url('discussion_detail', { did });
@@ -397,9 +396,10 @@ class DiscussionEditHandler extends DiscussionHandler {
else this.checkPerm(PERM.PERM_EDIT_DISCUSSION_SELF);
if (!this.user.hasPerm(PERM.PERM_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight;
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
const hidden = this.vnode.hidden ?? false;
await Promise.all([
discussion.edit(domainId, did, {
title, highlight, pin, content, editor: this.user._id, edited: true,
title, highlight, pin, content, editor: this.user._id, edited: true, hidden,
}),
oplog.log(this, 'discussion.edit', this.ddoc),
]);
+1
View File
@@ -462,6 +462,7 @@ declare module './model/discussion' {
sort: number;
lastRCount: number;
lock?: boolean;
hidden?: boolean;
}
}
+26 -4
View File
@@ -9,10 +9,12 @@ import * as bus from '../service/bus';
import db from '../service/db';
import { NumberKeys } from '../typeutils';
import { buildProjection } from '../utils';
import { PERM } from './builtin';
import * as contest from './contest';
import * as document from './document';
import problem from './problem';
import * as training from './training';
import { User } from './user';
export interface DiscussionDoc extends Document { }
export type Field = keyof DiscussionDoc;
@@ -20,7 +22,7 @@ export type Field = keyof DiscussionDoc;
export const PROJECTION_LIST: Field[] = [
'_id', 'domainId', 'docType', 'docId', 'highlight',
'nReply', 'views', 'pin', 'updateAt', 'owner',
'parentId', 'parentType', 'title',
'parentId', 'parentType', 'title', 'hidden',
];
export const PROJECTION_PUBLIC: Field[] = [
...PROJECTION_LIST, 'content', 'edited', 'react', 'maintainer',
@@ -43,7 +45,7 @@ export const coll = db.collection('discussion.history');
export async function add(
domainId: string, parentType: number, parentId: ObjectId | number | string,
owner: number, title: string, content: string,
ip: string | null = null, highlight: boolean, pin: boolean,
ip: string | null = null, highlight: boolean, pin: boolean, hidden = false,
): Promise<ObjectId> {
const time = new Date();
const payload: Partial<DiscussionDoc> = {
@@ -61,6 +63,7 @@ export async function add(
updateAt: time,
views: 0,
sort: 100,
hidden,
};
await bus.parallel('discussion/before-add', payload);
const res = await document.add(
@@ -271,9 +274,8 @@ export function flushNodes(domainId: string) {
export async function getVnode(domainId: string, type: number, id: string, uid?: number) {
if (type === document.TYPE_PROBLEM) {
let pdoc = await problem.get(domainId, Number.isSafeInteger(+id) ? +id : id, problem.PROJECTION_LIST);
const pdoc = await problem.get(domainId, Number.isSafeInteger(+id) ? +id : id, problem.PROJECTION_LIST);
if (!pdoc) throw new DiscussionNodeNotFoundError(id);
if (pdoc.hidden) pdoc = problem.default;
return { ...pdoc, type, id: pdoc.docId };
}
if ([document.TYPE_CONTEST, document.TYPE_TRAINING, document.TYPE_HOMEWORK].includes(type as any)) {
@@ -285,6 +287,7 @@ export async function getVnode(domainId: string, type: number, id: string, uid?:
const tsdoc = await model.getStatus(domainId, _id, uid);
tdoc.attend = tsdoc?.attend || tsdoc?.enroll;
}
if (type === document.TYPE_CONTEST) tdoc.hidden = !(contest.isDone(tdoc));
return { ...tdoc, type, id: _id };
}
return {
@@ -292,6 +295,7 @@ export async function getVnode(domainId: string, type: number, id: string, uid?:
...await getNode(domainId, id),
type,
id,
owner: 1,
};
}
@@ -312,6 +316,16 @@ export async function getListVnodes(domainId: string, ddocs: any, getHidden = fa
return res;
}
export function checkVNodeVisibility(type: number, vnode: any, user: User) {
if (type === document.TYPE_PROBLEM) {
if (vnode.hidden && !(user.own(vnode) || user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN))) return false;
}
if ([document.TYPE_CONTEST, document.TYPE_TRAINING, document.TYPE_HOMEWORK].includes(type as any)) {
if (!user.own(vnode) && vnode.assign?.length && !Set.intersection(vnode.assign, user.group).size) return false;
}
return true;
}
export function apply(ctx: Context) {
ctx.on('problem/delete', async (domainId, docId) => {
const dids = await document.getMulti(
@@ -328,6 +342,13 @@ export function apply(ctx: Context) {
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { docId: { $in: drids } }),
]);
});
ctx.on('problem/edit', async (result) => {
const dids = await document.getMulti(
result.domainId, document.TYPE_DISCUSSION,
{ parentType: document.TYPE_PROBLEM, parentId: result.docId },
).project({ docId: 1 }).map((ddoc) => ddoc.docId).toArray();
return await coll.updateMany({ _id: { $in: dids } }, { $set: { hidden: result.hidden } });
});
}
global.Hydro.model.discussion = {
@@ -367,4 +388,5 @@ global.Hydro.model.discussion = {
getNodes,
getVnode,
getListVnodes,
checkVNodeVisibility,
};
@@ -35,6 +35,9 @@
@
{{ datetimeSpan(ddoc.updateAt)|safe }}
</li>
{%- if ddoc.hidden -%}
<li class="text-orange">({{ _('Hidden') }})</li>
{%- endif -%}
</ul>
</div>
</div>