import loggerFactory from '../../utils/logger'; const { customTagUtils } = require('@growi/core'); const { OptionParser } = customTagUtils; const logger = loggerFactory('growi-plugin:attachment-refs:routes:refs'); const loginRequiredFallback = (req, res) => { return res.status(403).send('login required'); }; module.exports = (crowi) => { const express = crowi.require('express'); const mongoose = crowi.require('mongoose'); 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'); const router = express.Router(); const ObjectId = mongoose.Types.ObjectId; const User = crowi.model('User'); const Page = crowi.model('Page'); const Attachment = crowi.model('Attachment'); const { PageQueryBuilder } = Page; /** * generate RegExp instance by the 'expression' arg * @param {string} expression * @return {RegExp} */ function generateRegexp(expression) { // 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); 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}}$`), }); } /** * return an Attachment model */ router.get('/ref', accessTokenParser, loginRequired, async(req, res) => { const user = req.user; const { pagePath, fileNameOrId } = req.query; // eslint-disable-next-line no-unused-vars const options = JSON.parse(req.query.options); if (pagePath == null) { res.status(400).send('the param \'pagePath\' must be set.'); return; } const page = await Page.findByPathAndViewer(pagePath, user); // not found if (page == null) { res.status(404).send(`pagePath: '${pagePath}' is not found or forbidden.`); return; } // convert ObjectId const orConditions = [{ originalName: fileNameOrId }]; if (ObjectId.isValid(fileNameOrId)) { orConditions.push({ _id: ObjectId(fileNameOrId) }); } 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; } // serialize User data attachment.creator = serializeUserSecurely(attachment.creator); res.status(200).send({ attachment }); }); /** * return a list of Attachment */ router.get('/refs', accessTokenParser, loginRequired, async(req, res) => { const user = req.user; const { prefix, pagePath } = req.query; const options = JSON.parse(req.query.options); // 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; const regexOptionValue = options.regexp || options.regex; if (regexOptionValue != null) { try { regex = generateRegexp(regexOptionValue); } catch (err) { res.status(400).send(`the 'regex=${options.regex}' option is invalid as RegExp.`); return; } } let builder; // builder to retrieve descendance if (prefix != null) { builder = new PageQueryBuilder(Page.find()) .addConditionToListWithDescendants(prefix) .addConditionToExcludeTrashed() .addConditionToExcludeRedirect(); } // 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) { return res.status(400).send(err); } 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 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(); // serialize User data attachments.forEach((doc) => { if (doc.creator != null && doc.creator instanceof User) { doc.creator = serializeUserSecurely(doc.creator); } }); res.status(200).send({ attachments }); }); return router; };