|
@@ -1,84 +1,89 @@
|
|
|
-import type { IAttachment } from '@growi/core';
|
|
|
|
|
|
|
+import type { IPage, IUser, IAttachment } from '@growi/core';
|
|
|
|
|
+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 { model } from 'mongoose';
|
|
|
|
|
|
|
+import type { Request } from 'express';
|
|
|
|
|
+import { Router } from 'express';
|
|
|
|
|
+import type { Model, HydratedDocument } from 'mongoose';
|
|
|
|
|
+import mongoose, { model, Types } from 'mongoose';
|
|
|
|
|
+import { FilterXSS } from 'xss';
|
|
|
|
|
|
|
|
import loggerFactory from '../../utils/logger';
|
|
import loggerFactory from '../../utils/logger';
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
|
|
const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
|
|
|
|
|
|
|
|
|
|
|
|
|
-const loginRequiredFallback = (req, res) => {
|
|
|
|
|
- return res.status(403).send('login required');
|
|
|
|
|
-};
|
|
|
|
|
|
|
+function generateRegexp(expression: string): RegExp {
|
|
|
|
|
+ // https://regex101.com/r/uOrwqt/2
|
|
|
|
|
+ const matches = expression.match(/^\/(.+)\/(.*)?$/);
|
|
|
|
|
+
|
|
|
|
|
+ return (matches != null)
|
|
|
|
|
+ ? new RegExp(matches[1], matches[2])
|
|
|
|
|
+ : new RegExp(expression);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * add depth condition that limit fetched pages
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param {any} query
|
|
|
|
|
+ * @param {any} pagePath
|
|
|
|
|
+ * @param {any} optionsDepth
|
|
|
|
|
+ * @returns query
|
|
|
|
|
+ */
|
|
|
|
|
+function addDepthCondition(query, pagePath, optionsDepth) {
|
|
|
|
|
+ // when option strings is 'depth=', the option value is true
|
|
|
|
|
+ if (optionsDepth == null || optionsDepth === true) {
|
|
|
|
|
+ throw new Error('The value of depth option is invalid.');
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
|
|
|
-export const routesFactory = (crowi): any => {
|
|
|
|
|
- const express = crowi.require('express');
|
|
|
|
|
- const mongoose = crowi.require('mongoose');
|
|
|
|
|
|
|
+ const range = OptionParser.parseRange(optionsDepth);
|
|
|
|
|
|
|
|
- const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
|
|
|
|
|
- const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
|
|
|
|
|
- const { serializeUserSecurely } = crowi.require('../models/serializers/user-serializer');
|
|
|
|
|
|
|
+ if (range == null) {
|
|
|
|
|
+ return query;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const router = express.Router();
|
|
|
|
|
|
|
+ const start = range.start;
|
|
|
|
|
+ const end = range.end;
|
|
|
|
|
|
|
|
- const ObjectId = mongoose.Types.ObjectId;
|
|
|
|
|
|
|
+ if (start < 1 || end < 1) {
|
|
|
|
|
+ throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const User = crowi.model('User');
|
|
|
|
|
- const Page = crowi.model('Page');
|
|
|
|
|
|
|
+ // count slash
|
|
|
|
|
+ const slashNum = pagePath.split('/').length - 1;
|
|
|
|
|
+ const depthStart = slashNum; // start is not affect to fetch page
|
|
|
|
|
+ const depthEnd = slashNum + end - 1;
|
|
|
|
|
|
|
|
- const { PageQueryBuilder } = Page;
|
|
|
|
|
|
|
+ return query.and({
|
|
|
|
|
+ path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- function generateRegexp(expression: string): RegExp {
|
|
|
|
|
- // https://regex101.com/r/uOrwqt/2
|
|
|
|
|
- const matches = expression.match(/^\/(.+)\/(.*)?$/);
|
|
|
|
|
|
|
|
|
|
- return (matches != null)
|
|
|
|
|
- ? new RegExp(matches[1], matches[2])
|
|
|
|
|
- : new RegExp(expression);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+type RequestWithUser = Request & { user: HydratedDocument<IUser> };
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * add depth condition that limit fetched pages
|
|
|
|
|
- *
|
|
|
|
|
- * @param {any} query
|
|
|
|
|
- * @param {any} pagePath
|
|
|
|
|
- * @param {any} optionsDepth
|
|
|
|
|
- * @returns query
|
|
|
|
|
- */
|
|
|
|
|
- function addDepthCondition(query, pagePath, optionsDepth) {
|
|
|
|
|
- // when option strings is 'depth=', the option value is true
|
|
|
|
|
- if (optionsDepth == null || optionsDepth === true) {
|
|
|
|
|
- throw new Error('The value of depth option is invalid.');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const loginRequiredFallback = (req, res) => {
|
|
|
|
|
+ return res.status(403).send('login required');
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- const range = OptionParser.parseRange(optionsDepth);
|
|
|
|
|
|
|
+// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
|
|
|
+export const routesFactory = (crowi): any => {
|
|
|
|
|
|
|
|
- if (range == null) {
|
|
|
|
|
- return query;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
|
|
|
|
|
+ const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
|
|
|
|
|
|
|
|
- const start = range.start;
|
|
|
|
|
- const end = range.end;
|
|
|
|
|
|
|
+ const router = Router();
|
|
|
|
|
|
|
|
- if (start < 1 || end < 1) {
|
|
|
|
|
- throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const ObjectId = Types.ObjectId;
|
|
|
|
|
|
|
|
- // count slash
|
|
|
|
|
- const slashNum = pagePath.split('/').length - 1;
|
|
|
|
|
- const depthStart = slashNum; // start is not affect to fetch page
|
|
|
|
|
- const depthEnd = slashNum + end - 1;
|
|
|
|
|
|
|
+ const Page = mongoose.model <HydratedDocument<IPage>, Model<any> & any>('Page');
|
|
|
|
|
|
|
|
- return query.and({
|
|
|
|
|
- path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
+ const { PageQueryBuilder } = Page as any;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* return an Attachment model
|
|
* return an Attachment model
|
|
|
*/
|
|
*/
|
|
|
- router.get('/ref', accessTokenParser, loginRequired, async(req, res) => {
|
|
|
|
|
|
|
+ router.get('/ref', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
|
|
|
const user = req.user;
|
|
const user = req.user;
|
|
|
const { pagePath, fileNameOrId } = req.query;
|
|
const { pagePath, fileNameOrId } = req.query;
|
|
|
|
|
|
|
@@ -96,9 +101,10 @@ export const routesFactory = (crowi): any => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// convert ObjectId
|
|
// convert ObjectId
|
|
|
|
|
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
const orConditions: any[] = [{ originalName: fileNameOrId }];
|
|
const orConditions: any[] = [{ originalName: fileNameOrId }];
|
|
|
- if (ObjectId.isValid(fileNameOrId)) {
|
|
|
|
|
- orConditions.push({ _id: ObjectId(fileNameOrId) });
|
|
|
|
|
|
|
+ if (fileNameOrId != null && ObjectId.isValid(fileNameOrId.toString())) {
|
|
|
|
|
+ orConditions.push({ _id: new ObjectId(fileNameOrId.toString()) });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const Attachment = model<IAttachment>('Attachment');
|
|
const Attachment = model<IAttachment>('Attachment');
|
|
@@ -125,19 +131,16 @@ export const routesFactory = (crowi): any => {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // serialize User data
|
|
|
|
|
- attachment.creator = serializeUserSecurely(attachment.creator);
|
|
|
|
|
-
|
|
|
|
|
- res.status(200).send({ attachment });
|
|
|
|
|
|
|
+ res.status(200).send({ attachment: serializeAttachmentSecurely(attachment) });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* return a list of Attachment
|
|
* return a list of Attachment
|
|
|
*/
|
|
*/
|
|
|
- router.get('/refs', accessTokenParser, loginRequired, async(req, res) => {
|
|
|
|
|
|
|
+ router.get('/refs', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
|
|
|
const user = req.user;
|
|
const user = req.user;
|
|
|
const { prefix, pagePath } = req.query;
|
|
const { prefix, pagePath } = req.query;
|
|
|
- const options = JSON.parse(req.query.options);
|
|
|
|
|
|
|
+ const options: Record<string, string | undefined> = JSON.parse(req.query.options?.toString() ?? '');
|
|
|
|
|
|
|
|
// check either 'prefix' or 'pagePath ' is specified
|
|
// check either 'prefix' or 'pagePath ' is specified
|
|
|
if (prefix == null && pagePath == null) {
|
|
if (prefix == null && pagePath == null) {
|
|
@@ -146,14 +149,20 @@ export const routesFactory = (crowi): any => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// check regex
|
|
// check regex
|
|
|
- let regex;
|
|
|
|
|
- const regexOptionValue = options.regexp || options.regex;
|
|
|
|
|
|
|
+ let regex: RegExp | null = null;
|
|
|
|
|
+ const regexOptionValue = options.regexp ?? options.regex;
|
|
|
if (regexOptionValue != null) {
|
|
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 {
|
|
try {
|
|
|
regex = generateRegexp(regexOptionValue);
|
|
regex = generateRegexp(regexOptionValue);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
- res.status(400).send(`the 'regex=${options.regex}' option is invalid as RegExp.`);
|
|
|
|
|
|
|
+ res.status(400).send('the \'regex\' option is invalid as RegExp.');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -182,7 +191,8 @@ export const routesFactory = (crowi): any => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
- return res.status(400).send(err);
|
|
|
|
|
|
|
+ const filterXSS = new FilterXSS();
|
|
|
|
|
+ return res.status(400).send(filterXSS.process(err.toString()));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const results = await pageQuery.select('id').exec();
|
|
const results = await pageQuery.select('id').exec();
|
|
@@ -207,14 +217,7 @@ export const routesFactory = (crowi): any => {
|
|
|
.populate('creator')
|
|
.populate('creator')
|
|
|
.exec();
|
|
.exec();
|
|
|
|
|
|
|
|
- // serialize User data
|
|
|
|
|
- attachments.forEach((doc) => {
|
|
|
|
|
- if (doc.creator != null && doc.creator instanceof User) {
|
|
|
|
|
- doc.creator = serializeUserSecurely(doc.creator);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- res.status(200).send({ attachments });
|
|
|
|
|
|
|
+ res.status(200).send({ attachments: attachments.map(attachment => serializeAttachmentSecurely(attachment)) });
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
return router;
|
|
return router;
|