page.js 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212
  1. /* eslint-disable no-use-before-define */
  2. module.exports = function(crowi, app) {
  3. const debug = require('debug')('growi:routes:page');
  4. const logger = require('@alias/logger')('growi:routes:page');
  5. const swig = require('swig-templates');
  6. const pathUtils = require('growi-commons').pathUtils;
  7. const Page = crowi.model('Page');
  8. const User = crowi.model('User');
  9. const Bookmark = crowi.model('Bookmark');
  10. const PageTagRelation = crowi.model('PageTagRelation');
  11. const UpdatePost = crowi.model('UpdatePost');
  12. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  13. const ApiResponse = require('../util/apiResponse');
  14. const getToday = require('../util/getToday');
  15. const { configManager, slackNotificationService } = crowi;
  16. const interceptorManager = crowi.getInterceptorManager();
  17. const globalNotificationService = crowi.getGlobalNotificationService();
  18. const actions = {};
  19. const PORTAL_STATUS_NOT_EXISTS = 0;
  20. const PORTAL_STATUS_EXISTS = 1;
  21. const PORTAL_STATUS_FORBIDDEN = 2;
  22. // register page events
  23. const pageEvent = crowi.event('page');
  24. pageEvent.on('create', (page, user, socketClientId) => {
  25. page = serializeToObj(page); // eslint-disable-line no-param-reassign
  26. crowi.getIo().sockets.emit('page:create', { page, user, socketClientId });
  27. });
  28. pageEvent.on('update', (page, user, socketClientId) => {
  29. page = serializeToObj(page); // eslint-disable-line no-param-reassign
  30. crowi.getIo().sockets.emit('page:update', { page, user, socketClientId });
  31. });
  32. pageEvent.on('delete', (page, user, socketClientId) => {
  33. page = serializeToObj(page); // eslint-disable-line no-param-reassign
  34. crowi.getIo().sockets.emit('page:delete', { page, user, socketClientId });
  35. });
  36. function serializeToObj(page) {
  37. const returnObj = page.toObject();
  38. if (page.revisionHackmdSynced != null && page.revisionHackmdSynced._id != null) {
  39. returnObj.revisionHackmdSynced = page.revisionHackmdSynced._id;
  40. }
  41. if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
  42. returnObj.lastUpdateUser = page.lastUpdateUser.toObject();
  43. }
  44. if (page.creator != null && page.creator instanceof User) {
  45. returnObj.creator = page.creator.toObject();
  46. }
  47. return returnObj;
  48. }
  49. function getPathFromRequest(req) {
  50. return pathUtils.normalizePath(req.params[0] || '');
  51. }
  52. function isUserPage(path) {
  53. if (path.match(/^\/user\/[^/]+\/?$/)) {
  54. return true;
  55. }
  56. return false;
  57. }
  58. function generatePager(offset, limit, totalCount) {
  59. let next = null;
  60. let prev = null;
  61. if (offset > 0) {
  62. prev = offset - limit;
  63. if (prev < 0) {
  64. prev = 0;
  65. }
  66. }
  67. if (totalCount < limit) {
  68. next = null;
  69. }
  70. else {
  71. next = offset + limit;
  72. }
  73. return {
  74. prev,
  75. next,
  76. offset,
  77. };
  78. }
  79. // user notification
  80. // TODO create '/service/user-notification' module
  81. /**
  82. *
  83. * @param {Page} page
  84. * @param {User} user
  85. * @param {string} slackChannelsStr comma separated string. e.g. 'general,channel1,channel2'
  86. * @param {boolean} updateOrCreate
  87. * @param {string} previousRevision
  88. */
  89. async function notifyToSlackByUser(page, user, slackChannelsStr, updateOrCreate, previousRevision) {
  90. await page.updateSlackChannel(slackChannelsStr)
  91. .catch((err) => {
  92. logger.error('Error occured in updating slack channels: ', err);
  93. });
  94. if (slackNotificationService.hasSlackConfig()) {
  95. const slackChannels = slackChannelsStr != null ? slackChannelsStr.split(',') : [null];
  96. const promises = slackChannels.map((chan) => {
  97. return crowi.slack.postPage(page, user, chan, updateOrCreate, previousRevision);
  98. });
  99. Promise.all(promises)
  100. .catch((err) => {
  101. logger.error('Error occured in sending slack notification: ', err);
  102. });
  103. }
  104. }
  105. function addRendarVarsForPage(renderVars, page) {
  106. renderVars.page = page;
  107. renderVars.path = page.path;
  108. renderVars.revision = page.revision;
  109. renderVars.author = page.revision.author;
  110. renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
  111. renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
  112. renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
  113. }
  114. async function addRenderVarsForUserPage(renderVars, page, requestUser) {
  115. const userData = await User.findUserByUsername(User.getUsernameByPath(page.path))
  116. .populate(User.IMAGE_POPULATION);
  117. if (userData != null) {
  118. renderVars.pageUser = userData;
  119. renderVars.bookmarkList = await Bookmark.findByUser(userData, { limit: 10, populatePage: true, requestUser });
  120. }
  121. }
  122. function addRendarVarsForScope(renderVars, page) {
  123. renderVars.grant = page.grant;
  124. renderVars.grantedGroupId = page.grantedGroup ? page.grantedGroup.id : null;
  125. renderVars.grantedGroupName = page.grantedGroup ? page.grantedGroup.name : null;
  126. }
  127. async function addRenderVarsForSlack(renderVars, page) {
  128. renderVars.slack = await getSlackChannels(page);
  129. }
  130. async function addRenderVarsForDescendants(renderVars, path, requestUser, offset, limit, isRegExpEscapedFromPath) {
  131. const SEENER_THRESHOLD = 10;
  132. const queryOptions = {
  133. offset,
  134. limit: limit + 1,
  135. includeTrashed: path.startsWith('/trash/'),
  136. isRegExpEscapedFromPath,
  137. };
  138. const result = await Page.findListWithDescendants(path, requestUser, queryOptions);
  139. if (result.pages.length > limit) {
  140. result.pages.pop();
  141. }
  142. renderVars.viewConfig = {
  143. seener_threshold: SEENER_THRESHOLD,
  144. };
  145. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  146. renderVars.pages = pathUtils.encodePagesPath(result.pages);
  147. }
  148. function replacePlaceholdersOfTemplate(template, req) {
  149. if (req.user == null) {
  150. return '';
  151. }
  152. const definitions = {
  153. pagepath: getPathFromRequest(req),
  154. username: req.user.name,
  155. today: getToday(),
  156. };
  157. const compiledTemplate = swig.compile(template);
  158. return compiledTemplate(definitions);
  159. }
  160. async function showPageForPresentation(req, res, next) {
  161. const path = getPathFromRequest(req);
  162. const revisionId = req.query.revision;
  163. let page = await Page.findByPathAndViewer(path, req.user);
  164. if (page == null) {
  165. next();
  166. }
  167. const renderVars = {};
  168. // populate
  169. page = await page.populateDataToMakePresentation(revisionId);
  170. addRendarVarsForPage(renderVars, page);
  171. return res.render('page_presentation', renderVars);
  172. }
  173. async function showPageListForCrowiBehavior(req, res, next) {
  174. const portalPath = pathUtils.addTrailingSlash(getPathFromRequest(req));
  175. const revisionId = req.query.revision;
  176. // check whether this page has portal page
  177. const portalPageStatus = await getPortalPageState(portalPath, req.user);
  178. let view = 'customlayout-selector/page_list';
  179. const renderVars = { path: portalPath };
  180. if (portalPageStatus === PORTAL_STATUS_FORBIDDEN) {
  181. // inject to req
  182. req.isForbidden = true;
  183. view = 'customlayout-selector/forbidden';
  184. }
  185. else if (portalPageStatus === PORTAL_STATUS_EXISTS) {
  186. let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
  187. portalPage.initLatestRevisionField(revisionId);
  188. // populate
  189. portalPage = await portalPage.populateDataToShowRevision();
  190. addRendarVarsForPage(renderVars, portalPage);
  191. await addRenderVarsForSlack(renderVars, portalPage);
  192. }
  193. const limit = 50;
  194. const offset = parseInt(req.query.offset) || 0;
  195. await addRenderVarsForDescendants(renderVars, portalPath, req.user, offset, limit);
  196. await interceptorManager.process('beforeRenderPage', req, res, renderVars);
  197. return res.render(view, renderVars);
  198. }
  199. async function showPageForGrowiBehavior(req, res, next) {
  200. const path = getPathFromRequest(req);
  201. const revisionId = req.query.revision;
  202. let page = await Page.findByPathAndViewer(path, req.user);
  203. if (page == null) {
  204. // check the page is forbidden or just does not exist.
  205. req.isForbidden = await Page.count({ path }) > 0;
  206. return next();
  207. }
  208. if (page.redirectTo) {
  209. debug(`Redirect to '${page.redirectTo}'`);
  210. return res.redirect(encodeURI(`${page.redirectTo}?redirectFrom=${pathUtils.encodePagePath(path)}`));
  211. }
  212. logger.debug('Page is found when processing pageShowForGrowiBehavior', page._id, page.path);
  213. const limit = 50;
  214. const offset = parseInt(req.query.offset) || 0;
  215. const renderVars = {};
  216. let view = 'customlayout-selector/page';
  217. page.initLatestRevisionField(revisionId);
  218. // populate
  219. page = await page.populateDataToShowRevision();
  220. addRendarVarsForPage(renderVars, page);
  221. addRendarVarsForScope(renderVars, page);
  222. await addRenderVarsForSlack(renderVars, page);
  223. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  224. if (isUserPage(page.path)) {
  225. // change template
  226. view = 'customlayout-selector/user_page';
  227. await addRenderVarsForUserPage(renderVars, page, req.user);
  228. }
  229. await interceptorManager.process('beforeRenderPage', req, res, renderVars);
  230. return res.render(view, renderVars);
  231. }
  232. const getSlackChannels = async(page) => {
  233. if (page.extended.slack) {
  234. return page.extended.slack;
  235. }
  236. const data = await UpdatePost.findSettingsByPath(page.path);
  237. const channels = data.map((e) => { return e.channel }).join(', ');
  238. return channels;
  239. };
  240. /**
  241. *
  242. * @param {string} path
  243. * @param {User} user
  244. * @returns {number} PORTAL_STATUS_NOT_EXISTS(0) or PORTAL_STATUS_EXISTS(1) or PORTAL_STATUS_FORBIDDEN(2)
  245. */
  246. async function getPortalPageState(path, user) {
  247. const portalPath = Page.addSlashOfEnd(path);
  248. const page = await Page.findByPathAndViewer(portalPath, user);
  249. if (page == null) {
  250. // check the page is forbidden or just does not exist.
  251. const isForbidden = await Page.count({ path: portalPath }) > 0;
  252. return isForbidden ? PORTAL_STATUS_FORBIDDEN : PORTAL_STATUS_NOT_EXISTS;
  253. }
  254. return PORTAL_STATUS_EXISTS;
  255. }
  256. actions.showTopPage = function(req, res) {
  257. return showPageListForCrowiBehavior(req, res);
  258. };
  259. /**
  260. * switch action by behaviorType
  261. */
  262. /* eslint-disable no-else-return */
  263. actions.showPageWithEndOfSlash = function(req, res, next) {
  264. const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
  265. if (behaviorType === 'crowi') {
  266. return showPageListForCrowiBehavior(req, res, next);
  267. }
  268. else {
  269. const path = getPathFromRequest(req); // end of slash should be omitted
  270. // redirect and showPage action will be triggered
  271. return res.redirect(path);
  272. }
  273. };
  274. /* eslint-enable no-else-return */
  275. /**
  276. * switch action
  277. * - presentation mode
  278. * - by behaviorType
  279. */
  280. actions.showPage = async function(req, res, next) {
  281. // presentation mode
  282. if (req.query.presentation) {
  283. return showPageForPresentation(req, res, next);
  284. }
  285. const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
  286. // check whether this page has portal page
  287. if (behaviorType === 'crowi') {
  288. const portalPagePath = pathUtils.addTrailingSlash(getPathFromRequest(req));
  289. const hasPortalPage = await Page.count({ path: portalPagePath }) > 0;
  290. if (hasPortalPage) {
  291. logger.debug('The portal page is found', portalPagePath);
  292. return res.redirect(encodeURI(`${portalPagePath}?redirectFrom=${pathUtils.encodePagePath(req.path)}`));
  293. }
  294. }
  295. // delegate to showPageForGrowiBehavior
  296. return showPageForGrowiBehavior(req, res, next);
  297. };
  298. /**
  299. * switch action by behaviorType
  300. */
  301. /* eslint-disable no-else-return */
  302. actions.trashPageListShowWrapper = function(req, res) {
  303. const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
  304. if (behaviorType === 'crowi') {
  305. // Crowi behavior for '/trash/*'
  306. return actions.deletedPageListShow(req, res);
  307. }
  308. else {
  309. // redirect to '/trash'
  310. return res.redirect('/trash');
  311. }
  312. };
  313. /* eslint-enable no-else-return */
  314. /**
  315. * switch action by behaviorType
  316. */
  317. /* eslint-disable no-else-return */
  318. actions.trashPageShowWrapper = function(req, res) {
  319. const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
  320. if (behaviorType === 'crowi') {
  321. // redirect to '/trash/'
  322. return res.redirect('/trash/');
  323. }
  324. else {
  325. // Crowi behavior for '/trash/*'
  326. return actions.deletedPageListShow(req, res);
  327. }
  328. };
  329. /* eslint-enable no-else-return */
  330. /**
  331. * switch action by behaviorType
  332. */
  333. /* eslint-disable no-else-return */
  334. actions.deletedPageListShowWrapper = function(req, res) {
  335. const behaviorType = configManager.getConfig('crowi', 'customize:behavior');
  336. if (behaviorType === 'crowi') {
  337. // Crowi behavior for '/trash/*'
  338. return actions.deletedPageListShow(req, res);
  339. }
  340. else {
  341. const path = `/trash${getPathFromRequest(req)}`;
  342. return res.redirect(path);
  343. }
  344. };
  345. /* eslint-enable no-else-return */
  346. actions.notFound = async function(req, res) {
  347. const path = getPathFromRequest(req);
  348. const isCreatable = Page.isCreatableName(path);
  349. let view;
  350. const renderVars = { path };
  351. if (!isCreatable) {
  352. view = 'customlayout-selector/not_creatable';
  353. }
  354. else if (req.isForbidden) {
  355. view = 'customlayout-selector/forbidden';
  356. }
  357. else {
  358. view = 'customlayout-selector/not_found';
  359. // retrieve templates
  360. if (req.user != null) {
  361. const template = await Page.findTemplate(path);
  362. if (template.templateBody) {
  363. const body = replacePlaceholdersOfTemplate(template.templateBody, req);
  364. const tags = template.templateTags;
  365. renderVars.template = body;
  366. renderVars.templateTags = tags;
  367. }
  368. }
  369. // add scope variables by ancestor page
  370. const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
  371. if (ancestor != null) {
  372. await ancestor.populate('grantedGroup').execPopulate();
  373. addRendarVarsForScope(renderVars, ancestor);
  374. }
  375. }
  376. const limit = 50;
  377. const offset = parseInt(req.query.offset) || 0;
  378. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  379. return res.render(view, renderVars);
  380. };
  381. actions.deletedPageListShow = async function(req, res) {
  382. const path = `/trash${getPathFromRequest(req)}`;
  383. const limit = 50;
  384. const offset = parseInt(req.query.offset) || 0;
  385. const queryOptions = {
  386. offset,
  387. limit: limit + 1,
  388. includeTrashed: true,
  389. };
  390. const renderVars = {
  391. page: null,
  392. path,
  393. pages: [],
  394. };
  395. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  396. if (result.pages.length > limit) {
  397. result.pages.pop();
  398. }
  399. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  400. renderVars.pages = pathUtils.encodePagesPath(result.pages);
  401. res.render('customlayout-selector/page_list', renderVars);
  402. };
  403. /**
  404. * redirector
  405. */
  406. actions.redirector = async function(req, res) {
  407. const id = req.params.id;
  408. const page = await Page.findByIdAndViewer(id, req.user);
  409. if (page != null) {
  410. return res.redirect(pathUtils.encodePagePath(page.path));
  411. }
  412. return res.redirect('/');
  413. };
  414. const api = {};
  415. actions.api = api;
  416. /**
  417. * @api {get} /pages.list List pages by user
  418. * @apiName ListPage
  419. * @apiGroup Page
  420. *
  421. * @apiParam {String} path
  422. * @apiParam {String} user
  423. */
  424. api.list = async function(req, res) {
  425. const username = req.query.user || null;
  426. const path = req.query.path || null;
  427. const limit = +req.query.limit || 50;
  428. const offset = parseInt(req.query.offset) || 0;
  429. const queryOptions = { offset, limit: limit + 1 };
  430. // Accepts only one of these
  431. if (username === null && path === null) {
  432. return res.json(ApiResponse.error('Parameter user or path is required.'));
  433. }
  434. if (username !== null && path !== null) {
  435. return res.json(ApiResponse.error('Parameter user or path is required.'));
  436. }
  437. try {
  438. let result = null;
  439. if (path == null) {
  440. const user = await User.findUserByUsername(username);
  441. if (user === null) {
  442. throw new Error('The user not found.');
  443. }
  444. result = await Page.findListByCreator(user, req.user, queryOptions);
  445. }
  446. else {
  447. result = await Page.findListByStartWith(path, req.user, queryOptions);
  448. }
  449. if (result.pages.length > limit) {
  450. result.pages.pop();
  451. }
  452. result.pages = pathUtils.encodePagesPath(result.pages);
  453. return res.json(ApiResponse.success(result));
  454. }
  455. catch (err) {
  456. return res.json(ApiResponse.error(err));
  457. }
  458. };
  459. /**
  460. * @api {post} /pages.create Create new page
  461. * @apiName CreatePage
  462. * @apiGroup Page
  463. *
  464. * @apiParam {String} body
  465. * @apiParam {String} path
  466. * @apiParam {String} grant
  467. * @apiParam {Array} pageTags
  468. */
  469. api.create = async function(req, res) {
  470. const body = req.body.body || null;
  471. const pagePath = req.body.path || null;
  472. const grant = req.body.grant || null;
  473. const grantUserGroupId = req.body.grantUserGroupId || null;
  474. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  475. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  476. const slackChannels = req.body.slackChannels || null;
  477. const socketClientId = req.body.socketClientId || undefined;
  478. const pageTags = req.body.pageTags || undefined;
  479. if (body === null || pagePath === null) {
  480. return res.json(ApiResponse.error('Parameters body and path are required.'));
  481. }
  482. // check page existence
  483. const isExist = await Page.count({ path: pagePath }) > 0;
  484. if (isExist) {
  485. return res.json(ApiResponse.error('Page exists', 'already_exists'));
  486. }
  487. const options = { socketClientId };
  488. if (grant != null) {
  489. options.grant = grant;
  490. options.grantUserGroupId = grantUserGroupId;
  491. }
  492. const createdPage = await Page.create(pagePath, body, req.user, options);
  493. let savedTags;
  494. if (pageTags != null) {
  495. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  496. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  497. }
  498. const result = { page: serializeToObj(createdPage), tags: savedTags };
  499. res.json(ApiResponse.success(result));
  500. // update scopes for descendants
  501. if (overwriteScopesOfDescendants) {
  502. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  503. }
  504. // global notification
  505. try {
  506. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage.path, req.user);
  507. }
  508. catch (err) {
  509. logger.error(err);
  510. }
  511. // user notification
  512. if (isSlackEnabled) {
  513. await notifyToSlackByUser(createdPage, req.user, slackChannels, 'create', false);
  514. }
  515. };
  516. /**
  517. * @api {post} /pages.update Update page
  518. * @apiName UpdatePage
  519. * @apiGroup Page
  520. *
  521. * @apiParam {String} body
  522. * @apiParam {String} page_id
  523. * @apiParam {String} revision_id
  524. * @apiParam {String} grant
  525. *
  526. * In the case of the page exists:
  527. * - If revision_id is specified => update the page,
  528. * - If revision_id is not specified => force update by the new contents.
  529. */
  530. api.update = async function(req, res) {
  531. const pageBody = req.body.body || null;
  532. const pageId = req.body.page_id || null;
  533. const revisionId = req.body.revision_id || null;
  534. const grant = req.body.grant || null;
  535. const grantUserGroupId = req.body.grantUserGroupId || null;
  536. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  537. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  538. const slackChannels = req.body.slackChannels || null;
  539. const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd; // cast to boolean
  540. const socketClientId = req.body.socketClientId || undefined;
  541. const pageTags = req.body.pageTags || undefined;
  542. if (pageId === null || pageBody === null) {
  543. return res.json(ApiResponse.error('page_id and body are required.'));
  544. }
  545. // check page existence
  546. const isExist = await Page.count({ _id: pageId }) > 0;
  547. if (!isExist) {
  548. return res.json(ApiResponse.error(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  549. }
  550. // check revision
  551. let page = await Page.findByIdAndViewer(pageId, req.user);
  552. if (page != null && revisionId != null && !page.isUpdatable(revisionId)) {
  553. return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'outdated'));
  554. }
  555. const options = { isSyncRevisionToHackmd, socketClientId };
  556. if (grant != null) {
  557. options.grant = grant;
  558. options.grantUserGroupId = grantUserGroupId;
  559. }
  560. const Revision = crowi.model('Revision');
  561. const previousRevision = await Revision.findById(revisionId);
  562. try {
  563. page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
  564. }
  565. catch (err) {
  566. logger.error('error on _api/pages.update', err);
  567. return res.json(ApiResponse.error(err));
  568. }
  569. let savedTags;
  570. if (pageTags != null) {
  571. await PageTagRelation.updatePageTags(pageId, pageTags);
  572. savedTags = await PageTagRelation.listTagNamesByPage(pageId);
  573. }
  574. const result = { page: serializeToObj(page), tags: savedTags };
  575. res.json(ApiResponse.success(result));
  576. // update scopes for descendants
  577. if (overwriteScopesOfDescendants) {
  578. Page.applyScopesToDescendantsAsyncronously(page, req.user);
  579. }
  580. // global notification
  581. try {
  582. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page.path, req.user);
  583. }
  584. catch (err) {
  585. logger.error(err);
  586. }
  587. // user notification
  588. if (isSlackEnabled) {
  589. await notifyToSlackByUser(page, req.user, slackChannels, 'update', previousRevision);
  590. }
  591. };
  592. /**
  593. * @api {get} /pages.get Get page data
  594. * @apiName GetPage
  595. * @apiGroup Page
  596. *
  597. * @apiParam {String} page_id
  598. * @apiParam {String} path
  599. * @apiParam {String} revision_id
  600. */
  601. api.get = async function(req, res) {
  602. const pagePath = req.query.path || null;
  603. const pageId = req.query.page_id || null; // TODO: handling
  604. if (!pageId && !pagePath) {
  605. return res.json(ApiResponse.error(new Error('Parameter path or page_id is required.')));
  606. }
  607. let page;
  608. try {
  609. if (pageId) { // prioritized
  610. page = await Page.findByIdAndViewer(pageId, req.user);
  611. }
  612. else if (pagePath) {
  613. page = await Page.findByPathAndViewer(pagePath, req.user);
  614. }
  615. if (page == null) {
  616. throw new Error(`Page '${pageId || pagePath}' is not found or forbidden`, 'notfound_or_forbidden');
  617. }
  618. page.initLatestRevisionField();
  619. // populate
  620. page = await page.populateDataToShowRevision();
  621. }
  622. catch (err) {
  623. return res.json(ApiResponse.error(err));
  624. }
  625. const result = {};
  626. result.page = page; // TODO consider to use serializeToObj method -- 2018.08.06 Yuki Takei
  627. return res.json(ApiResponse.success(result));
  628. };
  629. /**
  630. * @api {get} /pages.exist Get if page exists
  631. * @apiName GetPage
  632. * @apiGroup Page
  633. *
  634. * @apiParam {String} pages (stringified JSON)
  635. */
  636. api.exist = async function(req, res) {
  637. const pagesAsObj = JSON.parse(req.query.pages || '{}');
  638. const pagePaths = Object.keys(pagesAsObj);
  639. await Promise.all(pagePaths.map(async(path) => {
  640. // check page existence
  641. const isExist = await Page.count({ path }) > 0;
  642. pagesAsObj[path] = isExist;
  643. return;
  644. }));
  645. const result = { pages: pagesAsObj };
  646. return res.json(ApiResponse.success(result));
  647. };
  648. /**
  649. * @api {get} /pages.getPageTag get page tags
  650. * @apiName GetPageTag
  651. * @apiGroup Page
  652. *
  653. * @apiParam {String} pageId
  654. */
  655. api.getPageTag = async function(req, res) {
  656. const result = {};
  657. try {
  658. result.tags = await PageTagRelation.listTagNamesByPage(req.query.pageId);
  659. }
  660. catch (err) {
  661. return res.json(ApiResponse.error(err));
  662. }
  663. return res.json(ApiResponse.success(result));
  664. };
  665. /**
  666. * @api {post} /pages.seen Mark as seen user
  667. * @apiName SeenPage
  668. * @apiGroup Page
  669. *
  670. * @apiParam {String} page_id Page Id.
  671. */
  672. api.seen = async function(req, res) {
  673. const user = req.user;
  674. const pageId = req.body.page_id;
  675. if (!pageId) {
  676. return res.json(ApiResponse.error('page_id required'));
  677. }
  678. if (!req.user) {
  679. return res.json(ApiResponse.error('user required'));
  680. }
  681. let page;
  682. try {
  683. page = await Page.findByIdAndViewer(pageId, user);
  684. if (user != null) {
  685. page = await page.seen(user);
  686. }
  687. }
  688. catch (err) {
  689. debug('Seen user update error', err);
  690. return res.json(ApiResponse.error(err));
  691. }
  692. const result = {};
  693. result.seenUser = page.seenUsers;
  694. return res.json(ApiResponse.success(result));
  695. };
  696. /**
  697. * @api {post} /likes.add Like page
  698. * @apiName LikePage
  699. * @apiGroup Page
  700. *
  701. * @apiParam {String} page_id Page Id.
  702. */
  703. api.like = async function(req, res) {
  704. const pageId = req.body.page_id;
  705. if (!pageId) {
  706. return res.json(ApiResponse.error('page_id required'));
  707. }
  708. if (!req.user) {
  709. return res.json(ApiResponse.error('user required'));
  710. }
  711. let page;
  712. try {
  713. page = await Page.findByIdAndViewer(pageId, req.user);
  714. if (page == null) {
  715. throw new Error(`Page '${pageId}' is not found or forbidden`);
  716. }
  717. page = await page.like(req.user);
  718. }
  719. catch (err) {
  720. debug('Seen user update error', err);
  721. return res.json(ApiResponse.error(err));
  722. }
  723. const result = { page };
  724. result.seenUser = page.seenUsers;
  725. res.json(ApiResponse.success(result));
  726. try {
  727. // global notification
  728. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page.path, req.user);
  729. }
  730. catch (err) {
  731. logger.error('Like failed', err);
  732. }
  733. };
  734. /**
  735. * @api {post} /likes.remove Unlike page
  736. * @apiName UnlikePage
  737. * @apiGroup Page
  738. *
  739. * @apiParam {String} page_id Page Id.
  740. */
  741. api.unlike = async function(req, res) {
  742. const pageId = req.body.page_id;
  743. if (!pageId) {
  744. return res.json(ApiResponse.error('page_id required'));
  745. }
  746. if (req.user == null) {
  747. return res.json(ApiResponse.error('user required'));
  748. }
  749. let page;
  750. try {
  751. page = await Page.findByIdAndViewer(pageId, req.user);
  752. if (page == null) {
  753. throw new Error(`Page '${pageId}' is not found or forbidden`);
  754. }
  755. page = await page.unlike(req.user);
  756. }
  757. catch (err) {
  758. debug('Seen user update error', err);
  759. return res.json(ApiResponse.error(err));
  760. }
  761. const result = { page };
  762. result.seenUser = page.seenUsers;
  763. return res.json(ApiResponse.success(result));
  764. };
  765. /**
  766. * @api {get} /pages.updatePost
  767. * @apiName Get UpdatePost setting list
  768. * @apiGroup Page
  769. *
  770. * @apiParam {String} path
  771. */
  772. api.getUpdatePost = function(req, res) {
  773. const path = req.query.path;
  774. const UpdatePost = crowi.model('UpdatePost');
  775. if (!path) {
  776. return res.json(ApiResponse.error({}));
  777. }
  778. UpdatePost.findSettingsByPath(path)
  779. .then((data) => {
  780. // eslint-disable-next-line no-param-reassign
  781. data = data.map((e) => {
  782. return e.channel;
  783. });
  784. debug('Found updatePost data', data);
  785. const result = { updatePost: data };
  786. return res.json(ApiResponse.success(result));
  787. })
  788. .catch((err) => {
  789. debug('Error occured while get setting', err);
  790. return res.json(ApiResponse.error({}));
  791. });
  792. };
  793. /**
  794. * @api {post} /pages.remove Remove page
  795. * @apiName RemovePage
  796. * @apiGroup Page
  797. *
  798. * @apiParam {String} page_id Page Id.
  799. * @apiParam {String} revision_id
  800. */
  801. api.remove = async function(req, res) {
  802. const pageId = req.body.page_id;
  803. const previousRevision = req.body.revision_id || null;
  804. const socketClientId = req.body.socketClientId || undefined;
  805. // get completely flag
  806. const isCompletely = (req.body.completely != null);
  807. // get recursively flag
  808. const isRecursively = (req.body.recursively != null);
  809. const options = { socketClientId };
  810. let page = await Page.findByIdAndViewer(pageId, req.user);
  811. if (page == null) {
  812. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  813. }
  814. debug('Delete page', page._id, page.path);
  815. try {
  816. if (isCompletely) {
  817. if (!req.user.canDeleteCompletely(page.creator)) {
  818. return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
  819. }
  820. if (isRecursively) {
  821. page = await Page.completelyDeletePageRecursively(page, req.user, options);
  822. }
  823. else {
  824. page = await Page.completelyDeletePage(page, req.user, options);
  825. }
  826. }
  827. else {
  828. if (!page.isUpdatable(previousRevision)) {
  829. return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
  830. }
  831. if (isRecursively) {
  832. page = await Page.deletePageRecursively(page, req.user, options);
  833. }
  834. else {
  835. page = await Page.deletePage(page, req.user, options);
  836. }
  837. }
  838. }
  839. catch (err) {
  840. logger.error('Error occured while get setting', err);
  841. return res.json(ApiResponse.error('Failed to delete page.', 'unknown'));
  842. }
  843. debug('Page deleted', page.path);
  844. const result = {};
  845. result.page = page; // TODO consider to use serializeToObj method -- 2018.08.06 Yuki Takei
  846. res.json(ApiResponse.success(result));
  847. // global notification
  848. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page.path, req.user);
  849. };
  850. /**
  851. * @api {post} /pages.revertRemove Revert removed page
  852. * @apiName RevertRemovePage
  853. * @apiGroup Page
  854. *
  855. * @apiParam {String} page_id Page Id.
  856. */
  857. api.revertRemove = async function(req, res, options) {
  858. const pageId = req.body.page_id;
  859. const socketClientId = req.body.socketClientId || undefined;
  860. // get recursively flag
  861. const isRecursively = (req.body.recursively !== undefined);
  862. let page;
  863. try {
  864. page = await Page.findByIdAndViewer(pageId, req.user);
  865. if (page == null) {
  866. throw new Error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden');
  867. }
  868. if (isRecursively) {
  869. page = await Page.revertDeletedPageRecursively(page, req.user, { socketClientId });
  870. }
  871. else {
  872. page = await Page.revertDeletedPage(page, req.user, { socketClientId });
  873. }
  874. }
  875. catch (err) {
  876. logger.error('Error occured while get setting', err);
  877. return res.json(ApiResponse.error('Failed to revert deleted page.'));
  878. }
  879. const result = {};
  880. result.page = page; // TODO consider to use serializeToObj method -- 2018.08.06 Yuki Takei
  881. return res.json(ApiResponse.success(result));
  882. };
  883. /**
  884. * @api {post} /pages.rename Rename page
  885. * @apiName RenamePage
  886. * @apiGroup Page
  887. *
  888. * @apiParam {String} page_id Page Id.
  889. * @apiParam {String} path
  890. * @apiParam {String} revision_id
  891. * @apiParam {String} new_path New path name.
  892. * @apiParam {Bool} create_redirect
  893. */
  894. api.rename = async function(req, res) {
  895. const pageId = req.body.page_id;
  896. const previousRevision = req.body.revision_id || null;
  897. const newPagePath = pathUtils.normalizePath(req.body.new_path);
  898. const options = {
  899. createRedirectPage: (req.body.create_redirect != null),
  900. updateMetadata: (req.body.remain_metadata == null),
  901. socketClientId: +req.body.socketClientId || undefined,
  902. };
  903. const isRecursively = (req.body.recursively != null);
  904. if (!Page.isCreatableName(newPagePath)) {
  905. return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
  906. }
  907. const isExist = await Page.count({ path: newPagePath }) > 0;
  908. if (isExist) {
  909. // if page found, cannot cannot rename to that path
  910. return res.json(ApiResponse.error(`'new_path=${newPagePath}' already exists`, 'already_exists'));
  911. }
  912. let page;
  913. try {
  914. page = await Page.findByIdAndViewer(pageId, req.user);
  915. if (page == null) {
  916. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  917. }
  918. if (!page.isUpdatable(previousRevision)) {
  919. return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
  920. }
  921. if (isRecursively) {
  922. page = await Page.renameRecursively(page, newPagePath, req.user, options);
  923. }
  924. else {
  925. page = await Page.rename(page, newPagePath, req.user, options);
  926. }
  927. }
  928. catch (err) {
  929. logger.error(err);
  930. return res.json(ApiResponse.error('Failed to update page.', 'unknown'));
  931. }
  932. const result = {};
  933. result.page = page; // TODO consider to use serializeToObj method -- 2018.08.06 Yuki Takei
  934. res.json(ApiResponse.success(result));
  935. // global notification
  936. globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page.path, req.user, {
  937. oldPath: req.body.path,
  938. });
  939. return page;
  940. };
  941. /**
  942. * @api {post} /pages.duplicate Duplicate page
  943. * @apiName DuplicatePage
  944. * @apiGroup Page
  945. *
  946. * @apiParam {String} page_id Page Id.
  947. * @apiParam {String} new_path New path name.
  948. */
  949. api.duplicate = async function(req, res) {
  950. const pageId = req.body.page_id;
  951. const newPagePath = pathUtils.normalizePath(req.body.new_path);
  952. const page = await Page.findByIdAndViewer(pageId, req.user);
  953. if (page == null) {
  954. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  955. }
  956. await page.populateDataToShowRevision();
  957. const originTags = await page.findRelatedTagsById();
  958. req.body.path = newPagePath;
  959. req.body.body = page.revision.body;
  960. req.body.grant = page.grant;
  961. req.body.grantedUsers = page.grantedUsers;
  962. req.body.grantUserGroupId = page.grantedGroup;
  963. req.body.pageTags = originTags;
  964. return api.create(req, res);
  965. };
  966. /**
  967. * @api {post} /pages.unlink Remove the redirecting page
  968. * @apiName UnlinkPage
  969. * @apiGroup Page
  970. *
  971. * @apiParam {String} page_id Page Id.
  972. * @apiParam {String} revision_id
  973. */
  974. api.unlink = async function(req, res) {
  975. const path = req.body.path;
  976. try {
  977. await Page.removeRedirectOriginPageByPath(path);
  978. logger.debug('Redirect Page deleted', path);
  979. }
  980. catch (err) {
  981. logger.error('Error occured while get setting', err);
  982. return res.json(ApiResponse.error('Failed to delete redirect page.'));
  983. }
  984. const result = { path };
  985. return res.json(ApiResponse.success(result));
  986. };
  987. api.recentCreated = async function(req, res) {
  988. const pageId = req.query.page_id;
  989. if (pageId == null) {
  990. return res.json(ApiResponse.error('param \'pageId\' must not be null'));
  991. }
  992. const page = await Page.findById(pageId);
  993. if (page == null) {
  994. return res.json(ApiResponse.error(`Page (id='${pageId}') does not exist`));
  995. }
  996. if (!isUserPage(page.path)) {
  997. return res.json(ApiResponse.error(`Page (id='${pageId}') is not a user home`));
  998. }
  999. const limit = +req.query.limit || 50;
  1000. const offset = +req.query.offset || 0;
  1001. const queryOptions = { offset, limit };
  1002. try {
  1003. const result = await Page.findListByCreator(page.creator, req.user, queryOptions);
  1004. result.pages = pathUtils.encodePagesPath(result.pages);
  1005. return res.json(ApiResponse.success(result));
  1006. }
  1007. catch (err) {
  1008. return res.json(ApiResponse.error(err));
  1009. }
  1010. };
  1011. return actions;
  1012. };