mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
core: strict discussion visiblility check (#537)
This commit is contained in:
1 parent
f39a28bfec
commit
b80c80d9b2
4 files changed
+44
-18
No files matched your search
@@ -1,7 +1,7 @@
|
|||||||
import { isSafeInteger } from 'lodash';
|
import { isSafeInteger } from 'lodash';
|
||||||
import { ObjectId } from 'mongodb';
|
import { ObjectId } from 'mongodb';
|
||||||
import {
|
import {
|
||||||
DiscussionLockedError, DiscussionNotFoundError, DocumentNotFoundError,
|
DiscussionLockedError, DiscussionNodeNotFoundError, DiscussionNotFoundError, DocumentNotFoundError,
|
||||||
PermissionError,
|
PermissionError,
|
||||||
} from '../error';
|
} from '../error';
|
||||||
import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface';
|
import { DiscussionDoc, DiscussionReplyDoc, DiscussionTailReplyDoc } from '../interface';
|
||||||
@@ -30,7 +30,7 @@ class DiscussionHandler extends Handler {
|
|||||||
vnode?: any;
|
vnode?: any;
|
||||||
|
|
||||||
@param('type', Types.Range(Object.keys(typeMapper)), true)
|
@param('type', Types.Range(Object.keys(typeMapper)), true)
|
||||||
@param('name', Types.Name, true)
|
@param('name', Types.String, true)
|
||||||
@param('did', Types.ObjectId, true)
|
@param('did', Types.ObjectId, true)
|
||||||
@param('drid', Types.ObjectId, true)
|
@param('drid', Types.ObjectId, true)
|
||||||
@param('drrid', 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(twd2): exclude problem/contest discussions?
|
||||||
// TODO(iceboy): continuation based pagination.
|
// TODO(iceboy): continuation based pagination.
|
||||||
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name, this.user._id);
|
this.vnode = await discussion.getVnode(domainId, typeMapper[type], name, this.user._id);
|
||||||
if (this.vnode.assign?.length && !this.user.own(this.vnode)) {
|
if (!discussion.checkVNodeVisibility(typeMapper[type], this.vnode, this.user)) throw new DiscussionNodeNotFoundError(this.vnode.id);
|
||||||
if (!Set.intersection(this.vnode.assign, this.user.group).size) {
|
|
||||||
throw new PermissionError(PERM.PERM_VIEW_PROBLEM_HIDDEN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.ddoc) {
|
if (this.ddoc) {
|
||||||
this.ddoc.parentType ||= this.vnode.type;
|
this.ddoc.parentType ||= this.vnode.type;
|
||||||
this.ddoc.parentId ||= this.vnode.id;
|
this.ddoc.parentId ||= this.vnode.id;
|
||||||
@@ -73,11 +68,13 @@ class DiscussionHandler extends Handler {
|
|||||||
|
|
||||||
class DiscussionMainHandler extends Handler {
|
class DiscussionMainHandler extends Handler {
|
||||||
@param('page', Types.PositiveInt, true)
|
@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
|
// Limit to known types
|
||||||
const parentType = { $in: Object.keys(typeMapper).map((i) => typeMapper[i]) };
|
const parentType = { $in: Object.keys(typeMapper).map((i) => typeMapper[i]) };
|
||||||
|
all &&= this.user.hasPerm(PERM.PERM_MOD_BADGE);
|
||||||
const [ddocs, dpcount] = await paginate(
|
const [ddocs, dpcount] = await paginate(
|
||||||
discussion.getMulti(domainId, { parentType }),
|
discussion.getMulti(domainId, { parentType, ...all ? {} : { hidden: false } }),
|
||||||
page,
|
page,
|
||||||
system.get('pagination.discussion'),
|
system.get('pagination.discussion'),
|
||||||
);
|
);
|
||||||
@@ -95,15 +92,16 @@ class DiscussionMainHandler extends Handler {
|
|||||||
|
|
||||||
class DiscussionNodeHandler extends DiscussionHandler {
|
class DiscussionNodeHandler extends DiscussionHandler {
|
||||||
@param('type', Types.Range(Object.keys(typeMapper)))
|
@param('type', Types.Range(Object.keys(typeMapper)))
|
||||||
@param('name', Types.Name)
|
@param('name', Types.String)
|
||||||
@param('page', Types.PositiveInt, true)
|
@param('page', Types.PositiveInt, true)
|
||||||
async get(domainId: string, type: string, _name: string, page = 1) {
|
async get(domainId: string, type: string, _name: string, page = 1) {
|
||||||
let name: ObjectId | string | number;
|
let name: ObjectId | string | number;
|
||||||
if (ObjectId.isValid(_name)) name = new ObjectId(_name);
|
if (ObjectId.isValid(_name)) name = new ObjectId(_name);
|
||||||
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
|
else if (isSafeInteger(parseInt(_name, 10))) name = parseInt(_name, 10);
|
||||||
else name = _name;
|
else name = _name;
|
||||||
|
const hidden = this.user.own(this.vnode) || this.user.hasPerm(PERM.PERM_EDIT_DISCUSSION) ? {} : { hidden: false };
|
||||||
const [ddocs, dpcount] = await paginate(
|
const [ddocs, dpcount] = await paginate(
|
||||||
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }),
|
discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name, ...hidden }),
|
||||||
page,
|
page,
|
||||||
system.get('pagination.discussion'),
|
system.get('pagination.discussion'),
|
||||||
);
|
);
|
||||||
@@ -152,9 +150,10 @@ class DiscussionCreateHandler extends DiscussionHandler {
|
|||||||
await this.limitRate('add_discussion', 3600, 60);
|
await this.limitRate('add_discussion', 3600, 60);
|
||||||
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
|
if (highlight) this.checkPerm(PERM.PERM_HIGHLIGHT_DISCUSSION);
|
||||||
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
|
if (pin) this.checkPerm(PERM.PERM_PIN_DISCUSSION);
|
||||||
|
const hidden = this.vnode.hidden ?? false;
|
||||||
const did = await discussion.add(
|
const did = await discussion.add(
|
||||||
domainId, typeMapper[type], this.vnode.id, this.user._id,
|
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.body = { did };
|
||||||
this.response.redirect = this.url('discussion_detail', { 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);
|
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_HIGHLIGHT_DISCUSSION)) highlight = this.ddoc.highlight;
|
||||||
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
|
if (!this.user.hasPerm(PERM.PERM_PIN_DISCUSSION)) pin = this.ddoc.pin;
|
||||||
|
const hidden = this.vnode.hidden ?? false;
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
discussion.edit(domainId, did, {
|
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),
|
oplog.log(this, 'discussion.edit', this.ddoc),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -462,6 +462,7 @@ declare module './model/discussion' {
|
|||||||
sort: number;
|
sort: number;
|
||||||
lastRCount: number;
|
lastRCount: number;
|
||||||
lock?: boolean;
|
lock?: boolean;
|
||||||
|
hidden?: boolean;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ import * as bus from '../service/bus';
|
|||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import { NumberKeys } from '../typeutils';
|
import { NumberKeys } from '../typeutils';
|
||||||
import { buildProjection } from '../utils';
|
import { buildProjection } from '../utils';
|
||||||
|
import { PERM } from './builtin';
|
||||||
import * as contest from './contest';
|
import * as contest from './contest';
|
||||||
import * as document from './document';
|
import * as document from './document';
|
||||||
import problem from './problem';
|
import problem from './problem';
|
||||||
import * as training from './training';
|
import * as training from './training';
|
||||||
|
import { User } from './user';
|
||||||
|
|
||||||
export interface DiscussionDoc extends Document { }
|
export interface DiscussionDoc extends Document { }
|
||||||
export type Field = keyof DiscussionDoc;
|
export type Field = keyof DiscussionDoc;
|
||||||
@@ -20,7 +22,7 @@ export type Field = keyof DiscussionDoc;
|
|||||||
export const PROJECTION_LIST: Field[] = [
|
export const PROJECTION_LIST: Field[] = [
|
||||||
'_id', 'domainId', 'docType', 'docId', 'highlight',
|
'_id', 'domainId', 'docType', 'docId', 'highlight',
|
||||||
'nReply', 'views', 'pin', 'updateAt', 'owner',
|
'nReply', 'views', 'pin', 'updateAt', 'owner',
|
||||||
'parentId', 'parentType', 'title',
|
'parentId', 'parentType', 'title', 'hidden',
|
||||||
];
|
];
|
||||||
export const PROJECTION_PUBLIC: Field[] = [
|
export const PROJECTION_PUBLIC: Field[] = [
|
||||||
...PROJECTION_LIST, 'content', 'edited', 'react', 'maintainer',
|
...PROJECTION_LIST, 'content', 'edited', 'react', 'maintainer',
|
||||||
@@ -43,7 +45,7 @@ export const coll = db.collection('discussion.history');
|
|||||||
export async function add(
|
export async function add(
|
||||||
domainId: string, parentType: number, parentId: ObjectId | number | string,
|
domainId: string, parentType: number, parentId: ObjectId | number | string,
|
||||||
owner: number, title: string, content: 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> {
|
): Promise<ObjectId> {
|
||||||
const time = new Date();
|
const time = new Date();
|
||||||
const payload: Partial<DiscussionDoc> = {
|
const payload: Partial<DiscussionDoc> = {
|
||||||
@@ -61,6 +63,7 @@ export async function add(
|
|||||||
updateAt: time,
|
updateAt: time,
|
||||||
views: 0,
|
views: 0,
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
hidden,
|
||||||
};
|
};
|
||||||
await bus.parallel('discussion/before-add', payload);
|
await bus.parallel('discussion/before-add', payload);
|
||||||
const res = await document.add(
|
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) {
|
export async function getVnode(domainId: string, type: number, id: string, uid?: number) {
|
||||||
if (type === document.TYPE_PROBLEM) {
|
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) throw new DiscussionNodeNotFoundError(id);
|
||||||
if (pdoc.hidden) pdoc = problem.default;
|
|
||||||
return { ...pdoc, type, id: pdoc.docId };
|
return { ...pdoc, type, id: pdoc.docId };
|
||||||
}
|
}
|
||||||
if ([document.TYPE_CONTEST, document.TYPE_TRAINING, document.TYPE_HOMEWORK].includes(type as any)) {
|
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);
|
const tsdoc = await model.getStatus(domainId, _id, uid);
|
||||||
tdoc.attend = tsdoc?.attend || tsdoc?.enroll;
|
tdoc.attend = tsdoc?.attend || tsdoc?.enroll;
|
||||||
}
|
}
|
||||||
|
if (type === document.TYPE_CONTEST) tdoc.hidden = !(contest.isDone(tdoc));
|
||||||
return { ...tdoc, type, id: _id };
|
return { ...tdoc, type, id: _id };
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -292,6 +295,7 @@ export async function getVnode(domainId: string, type: number, id: string, uid?:
|
|||||||
...await getNode(domainId, id),
|
...await getNode(domainId, id),
|
||||||
type,
|
type,
|
||||||
id,
|
id,
|
||||||
|
owner: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,6 +316,16 @@ export async function getListVnodes(domainId: string, ddocs: any, getHidden = fa
|
|||||||
return res;
|
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) {
|
export function apply(ctx: Context) {
|
||||||
ctx.on('problem/delete', async (domainId, docId) => {
|
ctx.on('problem/delete', async (domainId, docId) => {
|
||||||
const dids = await document.getMulti(
|
const dids = await document.getMulti(
|
||||||
@@ -328,6 +342,13 @@ export function apply(ctx: Context) {
|
|||||||
document.deleteMulti(domainId, document.TYPE_DISCUSSION_REPLY, { docId: { $in: drids } }),
|
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 = {
|
global.Hydro.model.discussion = {
|
||||||
@@ -367,4 +388,5 @@ global.Hydro.model.discussion = {
|
|||||||
getNodes,
|
getNodes,
|
||||||
getVnode,
|
getVnode,
|
||||||
getListVnodes,
|
getListVnodes,
|
||||||
|
checkVNodeVisibility,
|
||||||
};
|
};
|
||||||
@@ -35,6 +35,9 @@
|
|||||||
@
|
@
|
||||||
{{ datetimeSpan(ddoc.updateAt)|safe }}
|
{{ datetimeSpan(ddoc.updateAt)|safe }}
|
||||||
</li>
|
</li>
|
||||||
|
{%- if ddoc.hidden -%}
|
||||||
|
<li class="text-orange">({{ _('Hidden') }})</li>
|
||||||
|
{%- endif -%}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user