middlewares.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. // don't add any more middlewares to this file.
  2. // all new middlewares should be an independent file under /server/routes/middlewares
  3. const debug = require('debug')('growi:lib:middlewares');
  4. const logger = require('@alias/logger')('growi:lib:middlewares');
  5. const pathUtils = require('growi-commons').pathUtils;
  6. const md5 = require('md5');
  7. const entities = require('entities');
  8. module.exports = (crowi) => {
  9. const { configManager, appService } = crowi;
  10. const middlewares = {};
  11. middlewares.csrfKeyGenerator = function() {
  12. return function(req, res, next) {
  13. const csrfKey = (req.session && req.session.id) || 'anon';
  14. if (req.csrfToken === null) {
  15. req.csrfToken = crowi.getTokens().create(csrfKey);
  16. }
  17. next();
  18. };
  19. };
  20. middlewares.loginCheckerForPassport = function(req, res, next) {
  21. res.locals.user = req.user;
  22. next();
  23. };
  24. middlewares.csrfVerify = function(req, res, next) {
  25. const token = req.body._csrf || req.query._csrf || null;
  26. const csrfKey = (req.session && req.session.id) || 'anon';
  27. debug('req.skipCsrfVerify', req.skipCsrfVerify);
  28. if (req.skipCsrfVerify) {
  29. debug('csrf verify skipped');
  30. return next();
  31. }
  32. if (crowi.getTokens().verify(csrfKey, token)) {
  33. debug('csrf successfully verified');
  34. return next();
  35. }
  36. logger.warn('csrf verification failed. return 403', csrfKey, token);
  37. return res.sendStatus(403);
  38. };
  39. middlewares.swigFunctions = function() {
  40. return function(req, res, next) {
  41. require('../util/swigFunctions')(crowi, req, res.locals);
  42. next();
  43. };
  44. };
  45. middlewares.swigFilters = function(swig) {
  46. // define a function for Gravatar
  47. const generateGravatarSrc = function(user) {
  48. const email = user.email || '';
  49. const hash = md5(email.trim().toLowerCase());
  50. return `https://gravatar.com/avatar/${hash}`;
  51. };
  52. // define a function for uploaded picture
  53. const getUploadedPictureSrc = function(user) {
  54. if (user.image) {
  55. return user.image;
  56. }
  57. if (user.imageAttachment != null) {
  58. return user.imageAttachment.filePathProxied;
  59. }
  60. return '/images/icons/user.svg';
  61. };
  62. return function(req, res, next) {
  63. swig.setFilter('path2name', (string) => {
  64. const name = string.replace(/(\/)$/, '');
  65. if (name.match(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/)) { // /.../hoge/YYYY/MM/DD 形式のページ
  66. return name.replace(/.+\/([^/]+\/\d{4}\/\d{2}\/\d{2})$/, '$1');
  67. }
  68. if (name.match(/.+\/([^/]+\/\d{4}\/\d{2})$/)) { // /.../hoge/YYYY/MM 形式のページ
  69. return name.replace(/.+\/([^/]+\/\d{4}\/\d{2})$/, '$1');
  70. }
  71. if (name.match(/.+\/([^/]+\/\d{4})$/)) { // /.../hoge/YYYY 形式のページ
  72. return name.replace(/.+\/([^/]+\/\d{4})$/, '$1');
  73. }
  74. return name.replace(/.+\/(.+)?$/, '$1'); // ページの末尾を拾う
  75. });
  76. swig.setFilter('normalizeDateInPath', (path) => {
  77. const patterns = [
  78. [/20(\d{2})(\d{2})(\d{2})(.+)/g, '20$1/$2/$3/$4'],
  79. [/20(\d{2})(\d{2})(\d{2})/g, '20$1/$2/$3'],
  80. [/20(\d{2})(\d{2})(.+)/g, '20$1/$2/$3'],
  81. [/20(\d{2})(\d{2})/g, '20$1/$2'],
  82. [/20(\d{2})_(\d{1,2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3/$4'],
  83. [/20(\d{2})_(\d{1,2})_(\d{1,2})/g, '20$1/$2/$3'],
  84. [/20(\d{2})_(\d{1,2})_?(.+)/g, '20$1/$2/$3'],
  85. [/20(\d{2})_(\d{1,2})/g, '20$1/$2'],
  86. ];
  87. for (let i = 0; i < patterns.length; i++) {
  88. const mat = patterns[i][0];
  89. const rep = patterns[i][1];
  90. if (path.match(mat)) {
  91. return path.replace(mat, rep);
  92. }
  93. }
  94. return path;
  95. });
  96. swig.setFilter('datetz', (input, format) => {
  97. // timezone
  98. const swigFilters = require('swig-templates/lib/filters');
  99. return swigFilters.date(input, format, crowi.appService.getTzoffset());
  100. });
  101. swig.setFilter('nl2br', (string) => {
  102. return string
  103. .replace(/\n/g, '<br>');
  104. });
  105. swig.setFilter('removeTrailingSlash', (string) => {
  106. return pathUtils.removeTrailingSlash(string);
  107. });
  108. swig.setFilter('addTrailingSlash', (string) => {
  109. return pathUtils.addTrailingSlash(string);
  110. });
  111. swig.setFilter('presentation', (string) => {
  112. // 手抜き
  113. return string
  114. .replace(/\s(https?.+(jpe?g|png|gif))\s/, '\n\n\n![]($1)\n\n\n');
  115. });
  116. swig.setFilter('gravatar', generateGravatarSrc);
  117. swig.setFilter('uploadedpicture', getUploadedPictureSrc);
  118. swig.setFilter('picture', (user) => {
  119. if (!user) {
  120. return '/images/icons/user.svg';
  121. }
  122. if (user.isGravatarEnabled === true) {
  123. return generateGravatarSrc(user);
  124. }
  125. return getUploadedPictureSrc(user);
  126. });
  127. swig.setFilter('encodeHTML', (string) => {
  128. return entities.encodeHTML(string);
  129. });
  130. swig.setFilter('preventXss', (string) => {
  131. return crowi.xss.process(string);
  132. });
  133. swig.setFilter('slice', (list, start, end) => {
  134. return list.slice(start, end);
  135. });
  136. next();
  137. };
  138. };
  139. middlewares.adminRequired = function(req, res, next) {
  140. // check the user logged in
  141. // make sure that req.user isn't username/email string to login which is set by basic-auth-connect
  142. if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
  143. if (req.user.admin) {
  144. next();
  145. return;
  146. }
  147. return res.redirect('/');
  148. }
  149. return res.redirect('/login');
  150. };
  151. /**
  152. * require login handler
  153. *
  154. * @param {boolean} isStrictly whethere strictly restricted (default true)
  155. */
  156. middlewares.loginRequired = function(isStrictly = true) {
  157. return function(req, res, next) {
  158. const User = crowi.model('User');
  159. // when the route is not strictly restricted
  160. if (!isStrictly) {
  161. // when allowed to read
  162. if (crowi.aclService.getIsGuestAllowedToRead()) {
  163. return next();
  164. }
  165. }
  166. // check the user logged in
  167. // make sure that req.user isn't username/email string to login which is set by basic-auth-connect
  168. if (req.user != null && (req.user instanceof Object) && '_id' in req.user) {
  169. if (req.user.status === User.STATUS_ACTIVE) {
  170. // Active の人だけ先に進める
  171. return next();
  172. }
  173. if (req.user.status === User.STATUS_REGISTERED) {
  174. return res.redirect('/login/error/registered');
  175. }
  176. if (req.user.status === User.STATUS_SUSPENDED) {
  177. return res.redirect('/login/error/suspended');
  178. }
  179. if (req.user.status === User.STATUS_INVITED) {
  180. return res.redirect('/login/invited');
  181. }
  182. }
  183. // is api path
  184. const path = req.path || '';
  185. if (path.match(/^\/_api\/.+$/)) {
  186. return res.sendStatus(403);
  187. }
  188. req.session.jumpTo = req.originalUrl;
  189. return res.redirect('/login');
  190. };
  191. };
  192. middlewares.accessTokenParser = function(req, res, next) {
  193. // TODO: comply HTTP header of RFC6750 / Authorization: Bearer
  194. const accessToken = req.query.access_token || req.body.access_token || null;
  195. if (!accessToken) {
  196. return next();
  197. }
  198. const User = crowi.model('User');
  199. debug('accessToken is', accessToken);
  200. User.findUserByApiToken(accessToken)
  201. .then((userData) => {
  202. req.user = userData;
  203. req.skipCsrfVerify = true;
  204. debug('Access token parsed: skipCsrfVerify');
  205. next();
  206. })
  207. .catch((err) => {
  208. next();
  209. });
  210. };
  211. // this is for Installer
  212. middlewares.applicationNotInstalled = async function(req, res, next) {
  213. const isInstalled = await appService.isDBInitialized();
  214. if (isInstalled) {
  215. req.flash('errorMessage', 'Application already installed.');
  216. return res.redirect('admin'); // admin以外はadminRequiredで'/'にリダイレクトされる
  217. }
  218. return next();
  219. };
  220. middlewares.applicationInstalled = async function(req, res, next) {
  221. const isInstalled = await appService.isDBInitialized();
  222. if (!isInstalled) {
  223. return res.redirect('/installer');
  224. }
  225. return next();
  226. };
  227. middlewares.awsEnabled = function() {
  228. return function(req, res, next) {
  229. if (configManager.getConfig('crowi', 'aws:region') !== ''
  230. && configManager.getConfig('crowi', 'aws:bucket') !== ''
  231. && configManager.getConfig('crowi', 'aws:accessKeyId') !== ''
  232. && configManager.getConfig('crowi', 'aws:secretAccessKey') !== '') {
  233. req.flash('globalError', 'AWS settings required to use this function. Please ask the administrator.');
  234. return res.redirect('/');
  235. }
  236. return next();
  237. };
  238. };
  239. return middlewares;
  240. };