| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- 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 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';
- const logger = loggerFactory('growi:remark-attachment-refs:routes:refs');
- 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.');
- }
- const range = OptionParser.parseRange(optionsDepth);
- if (range == null) {
- return query;
- }
- const start = range.start;
- const end = range.end;
- if (start < 1 || end < 1) {
- throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
- }
- // count slash
- const slashNum = pagePath.split('/').length - 1;
- const depthStart = slashNum; // start is not affect to fetch page
- const depthEnd = slashNum + end - 1;
- return query.and({
- path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
- });
- }
- type RequestWithUser = Request & { user: HydratedDocument<IUser> };
- const loginRequiredFallback = (req, res) => {
- return res.status(403).send('login required');
- };
- // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
- export const routesFactory = (crowi): any => {
- const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
- const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
- const router = Router();
- const ObjectId = Types.ObjectId;
- const Page = mongoose.model <HydratedDocument<IPage>, Model<any> & any>('Page');
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const { PageQueryBuilder } = Page as any;
- /**
- * return an Attachment model
- */
- router.get('/ref', accessTokenParser, loginRequired, async(req: RequestWithUser, res) => {
- const user = req.user;
- const { pagePath, fileNameOrId } = req.query;
- 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(`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,
- $or: orConditions,
- })
- .populate('creator');
- // not found
- if (attachment == null) {
- res.status(404).send(`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
- */
- 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.');
- return;
- }
- try {
- regex = generateRegexp(regexOptionValue);
- }
- catch (err) {
- res.status(400).send('the \'regex\' option is invalid as RegExp.');
- return;
- }
- }
- 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 }));
- }
- Page.addConditionToFilteringByViewerForList(builder, user, false);
- let pageQuery = builder.query;
- // 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()));
- }
- const results = await pageQuery.select('id').exec();
- const pageIds = results.map(result => result.id);
- logger.debug('retrieve attachments for pages:', pageIds);
- // create query to find
- const Attachment = model<IAttachment>('Attachment');
- let query = Attachment
- .find({
- 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)) });
- });
- return router;
- };
|