|
@@ -1,9 +1,9 @@
|
|
|
-import type { IPage, IUser, IAttachment } from '@growi/core';
|
|
|
|
|
|
|
+import type { IAttachment, IPage, IUser } from '@growi/core';
|
|
|
import { serializeAttachmentSecurely } from '@growi/core/dist/models/serializers';
|
|
import { serializeAttachmentSecurely } from '@growi/core/dist/models/serializers';
|
|
|
import { OptionParser } from '@growi/core/dist/remark-plugins';
|
|
import { OptionParser } from '@growi/core/dist/remark-plugins';
|
|
|
import type { Request } from 'express';
|
|
import type { Request } from 'express';
|
|
|
import { Router } from 'express';
|
|
import { Router } from 'express';
|
|
|
-import type { Model, HydratedDocument } from 'mongoose';
|
|
|
|
|
|
|
+import type { HydratedDocument, Model } from 'mongoose';
|
|
|
import mongoose, { model, Types } from 'mongoose';
|
|
import mongoose, { model, Types } from 'mongoose';
|
|
|
import { FilterXSS } from 'xss';
|
|
import { FilterXSS } from 'xss';
|
|
|
|
|
|
|
@@ -11,12 +11,11 @@ import loggerFactory from '../../utils/logger';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
|
|
const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
|
|
|
|
|
|
|
|
-
|
|
|
|
|
function generateRegexp(expression: string): RegExp {
|
|
function generateRegexp(expression: string): RegExp {
|
|
|
// https://regex101.com/r/uOrwqt/2
|
|
// https://regex101.com/r/uOrwqt/2
|
|
|
const matches = expression.match(/^\/(.+)\/(.*)?$/);
|
|
const matches = expression.match(/^\/(.+)\/(.*)?$/);
|
|
|
|
|
|
|
|
- return (matches != null)
|
|
|
|
|
|
|
+ return matches != null
|
|
|
? new RegExp(matches[1], matches[2])
|
|
? new RegExp(matches[1], matches[2])
|
|
|
: new RegExp(expression);
|
|
: new RegExp(expression);
|
|
|
}
|
|
}
|
|
@@ -45,7 +44,9 @@ function addDepthCondition(query, pagePath, optionsDepth) {
|
|
|
const end = range.end;
|
|
const end = range.end;
|
|
|
|
|
|
|
|
if (start < 1 || end < 1) {
|
|
if (start < 1 || end < 1) {
|
|
|
- throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
|
|
|
|
|
|
|
+ throw new Error(
|
|
|
|
|
+ `specified depth is [${start}:${end}] : start and end are must be larger than 1`,
|
|
|
|
|
+ );
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// count slash
|
|
// count slash
|
|
@@ -58,7 +59,6 @@ function addDepthCondition(query, pagePath, optionsDepth) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
type RequestWithUser = Request & { user: HydratedDocument<IUser> };
|
|
type RequestWithUser = Request & { user: HydratedDocument<IUser> };
|
|
|
|
|
|
|
|
const loginRequiredFallback = (req, res) => {
|
|
const loginRequiredFallback = (req, res) => {
|
|
@@ -67,15 +67,20 @@ const loginRequiredFallback = (req, res) => {
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
|
export const routesFactory = (crowi): any => {
|
|
export const routesFactory = (crowi): any => {
|
|
|
-
|
|
|
|
|
- const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
|
|
|
|
|
|
|
+ const loginRequired = crowi.require('../middlewares/login-required')(
|
|
|
|
|
+ crowi,
|
|
|
|
|
+ true,
|
|
|
|
|
+ loginRequiredFallback,
|
|
|
|
|
+ );
|
|
|
const accessTokenParser = crowi.accessTokenParser;
|
|
const accessTokenParser = crowi.accessTokenParser;
|
|
|
|
|
|
|
|
const router = Router();
|
|
const router = Router();
|
|
|
|
|
|
|
|
const ObjectId = Types.ObjectId;
|
|
const ObjectId = Types.ObjectId;
|
|
|
|
|
|
|
|
- const Page = mongoose.model <HydratedDocument<IPage>, Model<any> & any>('Page');
|
|
|
|
|
|
|
+ const Page = mongoose.model<HydratedDocument<IPage>, Model<any> & any>(
|
|
|
|
|
+ 'Page',
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
const { PageQueryBuilder } = Page as any;
|
|
const { PageQueryBuilder } = Page as any;
|
|
@@ -83,143 +88,178 @@ export const routesFactory = (crowi): any => {
|
|
|
/**
|
|
/**
|
|
|
* return an Attachment model
|
|
* return an Attachment model
|
|
|
*/
|
|
*/
|
|
|
- router.get('/ref', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
|
|
|
|
|
- const user = req.user;
|
|
|
|
|
- const { pagePath, fileNameOrId } = req.query;
|
|
|
|
|
- const filterXSS = new FilterXSS();
|
|
|
|
|
-
|
|
|
|
|
- if (pagePath == null) {
|
|
|
|
|
- res.status(400).send('the param \'pagePath\' must be set.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const page = await Page.findByPathAndViewer(pagePath, user, undefined, true);
|
|
|
|
|
-
|
|
|
|
|
- // not found
|
|
|
|
|
- if (page == null) {
|
|
|
|
|
- res.status(404).send(filterXSS.process(`pagePath: '${pagePath}' is not found or forbidden.`));
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // convert ObjectId
|
|
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
- const orConditions: any[] = [{ originalName: fileNameOrId }];
|
|
|
|
|
- if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
|
|
|
|
|
- orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const Attachment = model<IAttachment>('Attachment');
|
|
|
|
|
- const attachment = await Attachment
|
|
|
|
|
- .findOne({
|
|
|
|
|
|
|
+ router.get(
|
|
|
|
|
+ '/ref',
|
|
|
|
|
+ accessTokenParser,
|
|
|
|
|
+ loginRequired,
|
|
|
|
|
+ async (req: RequestWithUser, res) => {
|
|
|
|
|
+ const user = req.user;
|
|
|
|
|
+ const { pagePath, fileNameOrId } = req.query;
|
|
|
|
|
+ const filterXSS = new FilterXSS();
|
|
|
|
|
+
|
|
|
|
|
+ if (pagePath == null) {
|
|
|
|
|
+ res.status(400).send("the param 'pagePath' must be set.");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const page = await Page.findByPathAndViewer(
|
|
|
|
|
+ pagePath,
|
|
|
|
|
+ user,
|
|
|
|
|
+ undefined,
|
|
|
|
|
+ true,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // not found
|
|
|
|
|
+ if (page == null) {
|
|
|
|
|
+ res
|
|
|
|
|
+ .status(404)
|
|
|
|
|
+ .send(
|
|
|
|
|
+ filterXSS.process(
|
|
|
|
|
+ `pagePath: '${pagePath}' is not found or forbidden.`,
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // convert ObjectId
|
|
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
+ const orConditions: any[] = [{ originalName: fileNameOrId }];
|
|
|
|
|
+ if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
|
|
|
|
|
+ orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const Attachment = model<IAttachment>('Attachment');
|
|
|
|
|
+ const attachment = await Attachment.findOne({
|
|
|
page: page._id,
|
|
page: page._id,
|
|
|
$or: orConditions,
|
|
$or: orConditions,
|
|
|
- })
|
|
|
|
|
- .populate('creator');
|
|
|
|
|
-
|
|
|
|
|
- // not found
|
|
|
|
|
- if (attachment == null) {
|
|
|
|
|
- res.status(404).send(filterXSS.process(`attachment '${fileNameOrId}' is not found.`));
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- logger.debug(`attachment '${attachment.id}' is found from fileNameOrId '${fileNameOrId}'`);
|
|
|
|
|
-
|
|
|
|
|
- // forbidden
|
|
|
|
|
- const isAccessible = await Page.isAccessiblePageByViewer(attachment.page, user);
|
|
|
|
|
- if (!isAccessible) {
|
|
|
|
|
- logger.debug(`attachment '${attachment.id}' is forbidden for user '${user && user.username}'`);
|
|
|
|
|
- res.status(403).send(`page '${attachment.page}' is forbidden.`);
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- res.status(200).send({ attachment: serializeAttachmentSecurely(attachment) });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ }).populate('creator');
|
|
|
|
|
+
|
|
|
|
|
+ // not found
|
|
|
|
|
+ if (attachment == null) {
|
|
|
|
|
+ res
|
|
|
|
|
+ .status(404)
|
|
|
|
|
+ .send(
|
|
|
|
|
+ filterXSS.process(`attachment '${fileNameOrId}' is not found.`),
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ logger.debug(
|
|
|
|
|
+ `attachment '${attachment.id}' is found from fileNameOrId '${fileNameOrId}'`,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // forbidden
|
|
|
|
|
+ const isAccessible = await Page.isAccessiblePageByViewer(
|
|
|
|
|
+ attachment.page,
|
|
|
|
|
+ user,
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!isAccessible) {
|
|
|
|
|
+ logger.debug(
|
|
|
|
|
+ `attachment '${attachment.id}' is forbidden for user '${user && user.username}'`,
|
|
|
|
|
+ );
|
|
|
|
|
+ res.status(403).send(`page '${attachment.page}' is forbidden.`);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res
|
|
|
|
|
+ .status(200)
|
|
|
|
|
+ .send({ attachment: serializeAttachmentSecurely(attachment) });
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* return a list of Attachment
|
|
* return a list of Attachment
|
|
|
*/
|
|
*/
|
|
|
- router.get('/refs', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
|
|
|
|
|
- const user = req.user;
|
|
|
|
|
- const { prefix, pagePath } = req.query;
|
|
|
|
|
- const options: Record<string, string | undefined> = JSON.parse(req.query.options?.toString() ?? '');
|
|
|
|
|
-
|
|
|
|
|
- // check either 'prefix' or 'pagePath ' is specified
|
|
|
|
|
- if (prefix == null && pagePath == null) {
|
|
|
|
|
- res.status(400).send('either the param \'prefix\' or \'pagePath\' must be set.');
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // check regex
|
|
|
|
|
- let regex: RegExp | null = null;
|
|
|
|
|
- const regexOptionValue = options.regexp ?? options.regex;
|
|
|
|
|
- if (regexOptionValue != null) {
|
|
|
|
|
- // check the length to avoid ReDoS
|
|
|
|
|
- if (regexOptionValue.length > 400) {
|
|
|
|
|
- res.status(400).send('the length of the \'regex\' option is too long.');
|
|
|
|
|
|
|
+ router.get(
|
|
|
|
|
+ '/refs',
|
|
|
|
|
+ accessTokenParser,
|
|
|
|
|
+ loginRequired,
|
|
|
|
|
+ async (req: RequestWithUser, res) => {
|
|
|
|
|
+ const user = req.user;
|
|
|
|
|
+ const { prefix, pagePath } = req.query;
|
|
|
|
|
+ const options: Record<string, string | undefined> = JSON.parse(
|
|
|
|
|
+ req.query.options?.toString() ?? '',
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // check either 'prefix' or 'pagePath ' is specified
|
|
|
|
|
+ if (prefix == null && pagePath == null) {
|
|
|
|
|
+ res
|
|
|
|
|
+ .status(400)
|
|
|
|
|
+ .send("either the param 'prefix' or 'pagePath' must be set.");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- try {
|
|
|
|
|
- regex = generateRegexp(regexOptionValue);
|
|
|
|
|
- }
|
|
|
|
|
- catch (err) {
|
|
|
|
|
- res.status(400).send('the \'regex\' option is invalid as RegExp.');
|
|
|
|
|
- return;
|
|
|
|
|
|
|
+ // check regex
|
|
|
|
|
+ let regex: RegExp | null = null;
|
|
|
|
|
+ const regexOptionValue = options.regexp ?? options.regex;
|
|
|
|
|
+ if (regexOptionValue != null) {
|
|
|
|
|
+ // check the length to avoid ReDoS
|
|
|
|
|
+ if (regexOptionValue.length > 400) {
|
|
|
|
|
+ res.status(400).send("the length of the 'regex' option is too long.");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ regex = generateRegexp(regexOptionValue);
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ res.status(400).send("the 'regex' option is invalid as RegExp.");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- let builder;
|
|
|
|
|
|
|
+ let builder;
|
|
|
|
|
|
|
|
- // builder to retrieve descendance
|
|
|
|
|
- if (prefix != null) {
|
|
|
|
|
- builder = new PageQueryBuilder(Page.find())
|
|
|
|
|
- .addConditionToListWithDescendants(prefix)
|
|
|
|
|
- .addConditionToExcludeTrashed();
|
|
|
|
|
- }
|
|
|
|
|
- // builder to get single page
|
|
|
|
|
- else {
|
|
|
|
|
- builder = new PageQueryBuilder(Page.find({ path: pagePath }));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // builder to retrieve descendance
|
|
|
|
|
+ if (prefix != null) {
|
|
|
|
|
+ builder = new PageQueryBuilder(Page.find())
|
|
|
|
|
+ .addConditionToListWithDescendants(prefix)
|
|
|
|
|
+ .addConditionToExcludeTrashed();
|
|
|
|
|
+ }
|
|
|
|
|
+ // builder to get single page
|
|
|
|
|
+ else {
|
|
|
|
|
+ builder = new PageQueryBuilder(Page.find({ path: pagePath }));
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- Page.addConditionToFilteringByViewerForList(builder, user, false);
|
|
|
|
|
|
|
+ Page.addConditionToFilteringByViewerForList(builder, user, false);
|
|
|
|
|
|
|
|
- let pageQuery = builder.query;
|
|
|
|
|
|
|
+ let pageQuery = builder.query;
|
|
|
|
|
|
|
|
- // depth
|
|
|
|
|
- try {
|
|
|
|
|
- if (prefix != null && options.depth != null) {
|
|
|
|
|
- pageQuery = addDepthCondition(pageQuery, prefix, options.depth);
|
|
|
|
|
|
|
+ // depth
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (prefix != null && options.depth != null) {
|
|
|
|
|
+ pageQuery = addDepthCondition(pageQuery, prefix, options.depth);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ const filterXSS = new FilterXSS();
|
|
|
|
|
+ return res.status(400).send(filterXSS.process(err.toString()));
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- catch (err) {
|
|
|
|
|
- const filterXSS = new FilterXSS();
|
|
|
|
|
- return res.status(400).send(filterXSS.process(err.toString()));
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- const results = await pageQuery.select('id').exec();
|
|
|
|
|
- const pageIds = results.map(result => result.id);
|
|
|
|
|
|
|
+ const results = await pageQuery.select('id').exec();
|
|
|
|
|
+ const pageIds = results.map((result) => result.id);
|
|
|
|
|
|
|
|
- logger.debug('retrieve attachments for pages:', pageIds);
|
|
|
|
|
|
|
+ logger.debug('retrieve attachments for pages:', pageIds);
|
|
|
|
|
|
|
|
- // create query to find
|
|
|
|
|
- const Attachment = model<IAttachment>('Attachment');
|
|
|
|
|
- let query = Attachment
|
|
|
|
|
- .find({
|
|
|
|
|
|
|
+ // create query to find
|
|
|
|
|
+ const Attachment = model<IAttachment>('Attachment');
|
|
|
|
|
+ let query = Attachment.find({
|
|
|
page: { $in: pageIds },
|
|
page: { $in: pageIds },
|
|
|
});
|
|
});
|
|
|
- // add regex condition
|
|
|
|
|
- if (regex != null) {
|
|
|
|
|
- query = query.and([
|
|
|
|
|
- { originalName: { $regex: regex } },
|
|
|
|
|
- ]);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const attachments = await query
|
|
|
|
|
- .populate('creator')
|
|
|
|
|
- .exec();
|
|
|
|
|
-
|
|
|
|
|
- res.status(200).send({ attachments: attachments.map(attachment => serializeAttachmentSecurely(attachment)) });
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // add regex condition
|
|
|
|
|
+ if (regex != null) {
|
|
|
|
|
+ query = query.and([{ originalName: { $regex: regex } }]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const attachments = await query.populate('creator').exec();
|
|
|
|
|
+
|
|
|
|
|
+ res
|
|
|
|
|
+ .status(200)
|
|
|
|
|
+ .send({
|
|
|
|
|
+ attachments: attachments.map((attachment) =>
|
|
|
|
|
+ serializeAttachmentSecurely(attachment),
|
|
|
|
|
+ ),
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
|
|
|
return router;
|
|
return router;
|
|
|
};
|
|
};
|