refs.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import loggerFactory from '../../utils/logger';
  2. const { customTagUtils } = require('@growi/core');
  3. const { OptionParser } = customTagUtils;
  4. const logger = loggerFactory('growi-plugin:attachment-refs:routes:refs');
  5. const loginRequiredFallback = (req, res) => {
  6. return res.status(403).send('login required');
  7. };
  8. module.exports = (crowi) => {
  9. const express = crowi.require('express');
  10. const mongoose = crowi.require('mongoose');
  11. const loginRequired = crowi.require('../middlewares/login-required')(crowi, true, loginRequiredFallback);
  12. const accessTokenParser = crowi.require('../middlewares/access-token-parser')(crowi);
  13. const { serializeUserSecurely } = crowi.require('../models/serializers/user-serializer');
  14. const router = express.Router();
  15. const ObjectId = mongoose.Types.ObjectId;
  16. const User = crowi.model('User');
  17. const Page = crowi.model('Page');
  18. const Attachment = crowi.model('Attachment');
  19. const { PageQueryBuilder } = Page;
  20. /**
  21. * generate RegExp instance by the 'expression' arg
  22. * @param {string} expression
  23. * @return {RegExp}
  24. */
  25. function generateRegexp(expression) {
  26. // https://regex101.com/r/uOrwqt/2
  27. const matches = expression.match(/^\/(.+)\/(.*)?$/);
  28. return (matches != null)
  29. ? new RegExp(matches[1], matches[2])
  30. : new RegExp(expression);
  31. }
  32. /**
  33. * add depth condition that limit fetched pages
  34. *
  35. * @param {any} query
  36. * @param {any} pagePath
  37. * @param {any} optionsDepth
  38. * @returns query
  39. */
  40. function addDepthCondition(query, pagePath, optionsDepth) {
  41. // when option strings is 'depth=', the option value is true
  42. if (optionsDepth == null || optionsDepth === true) {
  43. throw new Error('The value of depth option is invalid.');
  44. }
  45. const range = OptionParser.parseRange(optionsDepth);
  46. const start = range.start;
  47. const end = range.end;
  48. if (start < 1 || end < 1) {
  49. throw new Error(`specified depth is [${start}:${end}] : start and end are must be larger than 1`);
  50. }
  51. // count slash
  52. const slashNum = pagePath.split('/').length - 1;
  53. const depthStart = slashNum; // start is not affect to fetch page
  54. const depthEnd = slashNum + end - 1;
  55. return query.and({
  56. path: new RegExp(`^(\\/[^\\/]*){${depthStart},${depthEnd}}$`),
  57. });
  58. }
  59. /**
  60. * return an Attachment model
  61. */
  62. router.get('/ref', accessTokenParser, loginRequired, async(req, res) => {
  63. const user = req.user;
  64. const { pagePath, fileNameOrId } = req.query;
  65. // eslint-disable-next-line no-unused-vars
  66. const options = JSON.parse(req.query.options);
  67. if (pagePath == null) {
  68. res.status(400).send('the param \'pagePath\' must be set.');
  69. return;
  70. }
  71. const page = await Page.findByPathAndViewer(pagePath, user);
  72. // not found
  73. if (page == null) {
  74. res.status(404).send(`pagePath: '${pagePath}' is not found or forbidden.`);
  75. return;
  76. }
  77. // convert ObjectId
  78. const orConditions = [{ originalName: fileNameOrId }];
  79. if (ObjectId.isValid(fileNameOrId)) {
  80. orConditions.push({ _id: ObjectId(fileNameOrId) });
  81. }
  82. const attachment = await Attachment
  83. .findOne({
  84. page: page._id,
  85. $or: orConditions,
  86. })
  87. .populate('creator');
  88. // not found
  89. if (attachment == null) {
  90. res.status(404).send(`attachment '${fileNameOrId}' is not found.`);
  91. return;
  92. }
  93. logger.debug(`attachment '${attachment.id}' is found from fileNameOrId '${fileNameOrId}'`);
  94. // forbidden
  95. const isAccessible = await Page.isAccessiblePageByViewer(attachment.page, user);
  96. if (!isAccessible) {
  97. logger.debug(`attachment '${attachment.id}' is forbidden for user '${user && user.username}'`);
  98. res.status(403).send(`page '${attachment.page}' is forbidden.`);
  99. return;
  100. }
  101. // serialize User data
  102. attachment.creator = serializeUserSecurely(attachment.creator);
  103. res.status(200).send({ attachment });
  104. });
  105. /**
  106. * return a list of Attachment
  107. */
  108. router.get('/refs', accessTokenParser, loginRequired, async(req, res) => {
  109. const user = req.user;
  110. const { prefix, pagePath } = req.query;
  111. const options = JSON.parse(req.query.options);
  112. // check either 'prefix' or 'pagePath ' is specified
  113. if (prefix == null && pagePath == null) {
  114. res.status(400).send('either the param \'prefix\' or \'pagePath\' must be set.');
  115. return;
  116. }
  117. // check regex
  118. let regex;
  119. const regexOptionValue = options.regexp || options.regex;
  120. if (regexOptionValue != null) {
  121. try {
  122. regex = generateRegexp(regexOptionValue);
  123. }
  124. catch (err) {
  125. res.status(400).send(`the 'regex=${options.regex}' option is invalid as RegExp.`);
  126. return;
  127. }
  128. }
  129. let builder;
  130. // builder to retrieve descendance
  131. if (prefix != null) {
  132. builder = new PageQueryBuilder(Page.find())
  133. .addConditionToListWithDescendants(prefix)
  134. .addConditionToExcludeTrashed()
  135. .addConditionToExcludeRedirect();
  136. }
  137. // builder to get single page
  138. else {
  139. builder = new PageQueryBuilder(Page.find({ path: pagePath }));
  140. }
  141. Page.addConditionToFilteringByViewerForList(builder, user, false);
  142. let pageQuery = builder.query;
  143. // depth
  144. try {
  145. if (prefix != null && options.depth != null) {
  146. pageQuery = addDepthCondition(pageQuery, prefix, options.depth);
  147. }
  148. }
  149. catch (err) {
  150. return res.status(400).send(err);
  151. }
  152. const results = await pageQuery.select('id').exec();
  153. const pageIds = results.map(result => result.id);
  154. logger.debug('retrieve attachments for pages:', pageIds);
  155. // create query to find
  156. let query = Attachment
  157. .find({
  158. page: { $in: pageIds },
  159. });
  160. // add regex condition
  161. if (regex != null) {
  162. query = query.and({
  163. originalName: { $regex: regex },
  164. });
  165. }
  166. const attachments = await query
  167. .populate('creator')
  168. .exec();
  169. // serialize User data
  170. attachments.forEach((doc) => {
  171. if (doc.creator != null && doc.creator instanceof User) {
  172. doc.creator = serializeUserSecurely(doc.creator);
  173. }
  174. });
  175. res.status(200).send({ attachments });
  176. });
  177. return router;
  178. };