page.js 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444
  1. const { serializePageSecurely } = require('../models/serializers/page-serializer');
  2. const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
  3. /**
  4. * @swagger
  5. * tags:
  6. * name: Pages
  7. */
  8. /**
  9. * @swagger
  10. *
  11. * components:
  12. * schemas:
  13. * Page:
  14. * description: Page
  15. * type: object
  16. * properties:
  17. * _id:
  18. * type: string
  19. * description: page ID
  20. * example: 5e07345972560e001761fa63
  21. * __v:
  22. * type: number
  23. * description: DB record version
  24. * example: 0
  25. * commentCount:
  26. * type: number
  27. * description: count of comments
  28. * example: 3
  29. * createdAt:
  30. * type: string
  31. * description: date created at
  32. * example: 2010-01-01T00:00:00.000Z
  33. * creator:
  34. * $ref: '#/components/schemas/User'
  35. * extended:
  36. * type: object
  37. * description: extend data
  38. * example: {}
  39. * grant:
  40. * type: number
  41. * description: grant
  42. * example: 1
  43. * grantedUsers:
  44. * type: array
  45. * description: granted users
  46. * items:
  47. * type: string
  48. * description: user ID
  49. * example: ["5ae5fccfc5577b0004dbd8ab"]
  50. * lastUpdateUser:
  51. * $ref: '#/components/schemas/User'
  52. * liker:
  53. * type: array
  54. * description: granted users
  55. * items:
  56. * type: string
  57. * description: user ID
  58. * example: []
  59. * path:
  60. * type: string
  61. * description: page path
  62. * example: /
  63. * redirectTo:
  64. * type: string
  65. * description: redirect path
  66. * example: ""
  67. * revision:
  68. * $ref: '#/components/schemas/Revision'
  69. * status:
  70. * type: string
  71. * description: status
  72. * enum:
  73. * - 'wip'
  74. * - 'published'
  75. * - 'deleted'
  76. * - 'deprecated'
  77. * example: published
  78. * updatedAt:
  79. * type: string
  80. * description: date updated at
  81. * example: 2010-01-01T00:00:00.000Z
  82. *
  83. * UpdatePost:
  84. * description: UpdatePost
  85. * type: object
  86. * properties:
  87. * _id:
  88. * type: string
  89. * description: update post ID
  90. * example: 5e0734e472560e001761fa68
  91. * __v:
  92. * type: number
  93. * description: DB record version
  94. * example: 0
  95. * pathPattern:
  96. * type: string
  97. * description: path pattern
  98. * example: /test
  99. * patternPrefix:
  100. * type: string
  101. * description: patternPrefix prefix
  102. * example: /
  103. * patternPrefix2:
  104. * type: string
  105. * description: path
  106. * example: test
  107. * channel:
  108. * type: string
  109. * description: channel
  110. * example: general
  111. * provider:
  112. * type: string
  113. * description: provider
  114. * enum:
  115. * - slack
  116. * example: slack
  117. * creator:
  118. * $ref: '#/components/schemas/User'
  119. * createdAt:
  120. * type: string
  121. * description: date created at
  122. * example: 2010-01-01T00:00:00.000Z
  123. */
  124. /* eslint-disable no-use-before-define */
  125. module.exports = function(crowi, app) {
  126. const debug = require('debug')('growi:routes:page');
  127. const logger = require('@alias/logger')('growi:routes:page');
  128. const swig = require('swig-templates');
  129. const pathUtils = require('growi-commons').pathUtils;
  130. const Page = crowi.model('Page');
  131. const User = crowi.model('User');
  132. const Bookmark = crowi.model('Bookmark');
  133. const PageTagRelation = crowi.model('PageTagRelation');
  134. const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
  135. const ShareLink = crowi.model('ShareLink');
  136. const ApiResponse = require('../util/apiResponse');
  137. const getToday = require('../util/getToday');
  138. const { configManager, xssService } = crowi;
  139. const interceptorManager = crowi.getInterceptorManager();
  140. const globalNotificationService = crowi.getGlobalNotificationService();
  141. const userNotificationService = crowi.getUserNotificationService();
  142. const XssOption = require('../../lib/service/xss/xssOption');
  143. const Xss = require('../../lib/service/xss/index');
  144. const initializedConfig = {
  145. isEnabledXssPrevention: configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
  146. tagWhiteList: xssService.getTagWhiteList(),
  147. attrWhiteList: xssService.getAttrWhiteList(),
  148. };
  149. const xssOption = new XssOption(initializedConfig);
  150. const xss = new Xss(xssOption);
  151. const actions = {};
  152. function getPathFromRequest(req) {
  153. return pathUtils.normalizePath(req.params[0] || '');
  154. }
  155. function isUserPage(path) {
  156. if (path.match(/^\/user\/[^/]+\/?$/)) {
  157. return true;
  158. }
  159. return false;
  160. }
  161. function generatePager(offset, limit, totalCount) {
  162. let prev = null;
  163. if (offset > 0) {
  164. prev = offset - limit;
  165. if (prev < 0) {
  166. prev = 0;
  167. }
  168. }
  169. let next = offset + limit;
  170. if (totalCount < next) {
  171. next = null;
  172. }
  173. return {
  174. prev,
  175. next,
  176. offset,
  177. };
  178. }
  179. function addRenderVarsForPage(renderVars, page) {
  180. renderVars.page = page;
  181. renderVars.revision = page.revision;
  182. renderVars.pageIdOnHackmd = page.pageIdOnHackmd;
  183. renderVars.revisionHackmdSynced = page.revisionHackmdSynced;
  184. renderVars.hasDraftOnHackmd = page.hasDraftOnHackmd;
  185. if (page.creator != null) {
  186. renderVars.page.creator = renderVars.page.creator.toObject();
  187. }
  188. if (page.revision.author != null) {
  189. renderVars.revision.author = renderVars.revision.author.toObject();
  190. }
  191. if (page.deleteUser != null) {
  192. renderVars.page.deleteUser = renderVars.page.deleteUser.toObject();
  193. }
  194. }
  195. function addRenderVarsForPresentation(renderVars, page) {
  196. // sanitize page.revision.body
  197. if (crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention')) {
  198. const preventXssRevision = xss.process(page.revision.body);
  199. page.revision.body = preventXssRevision;
  200. }
  201. renderVars.page = page;
  202. renderVars.revision = page.revision;
  203. }
  204. async function addRenderVarsForUserPage(renderVars, page, requestUser) {
  205. const userData = await User.findUserByUsername(User.getUsernameByPath(page.path));
  206. if (userData != null) {
  207. renderVars.pageUser = userData.toObject();
  208. renderVars.bookmarkList = await Bookmark.findByUser(userData, { limit: 10, populatePage: true, requestUser });
  209. }
  210. }
  211. function addRenderVarsForScope(renderVars, page) {
  212. renderVars.grant = page.grant;
  213. renderVars.grantedGroupId = page.grantedGroup ? page.grantedGroup.id : null;
  214. renderVars.grantedGroupName = page.grantedGroup ? page.grantedGroup.name : null;
  215. }
  216. async function addRenderVarsForDescendants(renderVars, path, requestUser, offset, limit, isRegExpEscapedFromPath) {
  217. const SEENER_THRESHOLD = 10;
  218. const queryOptions = {
  219. offset,
  220. limit,
  221. includeTrashed: path.startsWith('/trash/'),
  222. isRegExpEscapedFromPath,
  223. };
  224. const result = await Page.findListWithDescendants(path, requestUser, queryOptions);
  225. if (result.pages.length > limit) {
  226. result.pages.pop();
  227. }
  228. renderVars.viewConfig = {
  229. seener_threshold: SEENER_THRESHOLD,
  230. };
  231. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  232. renderVars.pages = result.pages;
  233. }
  234. function replacePlaceholdersOfTemplate(template, req) {
  235. if (req.user == null) {
  236. return '';
  237. }
  238. const definitions = {
  239. pagepath: getPathFromRequest(req),
  240. username: req.user.name,
  241. today: getToday(),
  242. };
  243. const compiledTemplate = swig.compile(template);
  244. return compiledTemplate(definitions);
  245. }
  246. async function showPageForPresentation(req, res, next) {
  247. const path = getPathFromRequest(req);
  248. const { revisionId } = req.query;
  249. let page = await Page.findByPathAndViewer(path, req.user);
  250. if (page == null) {
  251. next();
  252. }
  253. const renderVars = {};
  254. // populate
  255. page = await page.populateDataToMakePresentation(revisionId);
  256. if (page != null) {
  257. addRenderVarsForPresentation(renderVars, page);
  258. }
  259. return res.render('page_presentation', renderVars);
  260. }
  261. async function showTopPage(req, res, next) {
  262. const portalPath = req.path;
  263. const revisionId = req.query.revision;
  264. const view = 'layout-growi/page_list';
  265. const renderVars = { path: portalPath };
  266. let portalPage = await Page.findByPathAndViewer(portalPath, req.user);
  267. portalPage.initLatestRevisionField(revisionId);
  268. // add user to seen users
  269. if (req.user != null) {
  270. portalPage = await portalPage.seen(req.user);
  271. }
  272. // populate
  273. portalPage = await portalPage.populateDataToShowRevision();
  274. addRenderVarsForPage(renderVars, portalPage);
  275. const sharelinksNumber = await ShareLink.countDocuments({ relatedPage: portalPage._id });
  276. renderVars.sharelinksNumber = sharelinksNumber;
  277. const limit = 50;
  278. const offset = parseInt(req.query.offset) || 0;
  279. await addRenderVarsForDescendants(renderVars, portalPath, req.user, offset, limit);
  280. await interceptorManager.process('beforeRenderPage', req, res, renderVars);
  281. return res.render(view, renderVars);
  282. }
  283. async function showPageForGrowiBehavior(req, res, next) {
  284. const path = getPathFromRequest(req);
  285. const revisionId = req.query.revision;
  286. let page = await Page.findByPathAndViewer(path, req.user);
  287. if (page == null) {
  288. // check the page is forbidden or just does not exist.
  289. req.isForbidden = await Page.count({ path }) > 0;
  290. return next();
  291. }
  292. if (page.redirectTo) {
  293. debug(`Redirect to '${page.redirectTo}'`);
  294. return res.redirect(`${encodeURI(page.redirectTo)}?redirectFrom=${encodeURIComponent(path)}`);
  295. }
  296. logger.debug('Page is found when processing pageShowForGrowiBehavior', page._id, page.path);
  297. const limit = 50;
  298. const offset = parseInt(req.query.offset) || 0;
  299. const renderVars = {};
  300. let view = 'layout-growi/page';
  301. page.initLatestRevisionField(revisionId);
  302. // add user to seen users
  303. if (req.user != null) {
  304. page = await page.seen(req.user);
  305. }
  306. // populate
  307. page = await page.populateDataToShowRevision();
  308. addRenderVarsForPage(renderVars, page);
  309. addRenderVarsForScope(renderVars, page);
  310. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  311. const sharelinksNumber = await ShareLink.countDocuments({ relatedPage: page._id });
  312. renderVars.sharelinksNumber = sharelinksNumber;
  313. if (isUserPage(page.path)) {
  314. // change template
  315. view = 'layout-growi/user_page';
  316. await addRenderVarsForUserPage(renderVars, page, req.user);
  317. }
  318. await interceptorManager.process('beforeRenderPage', req, res, renderVars);
  319. return res.render(view, renderVars);
  320. }
  321. actions.showTopPage = function(req, res) {
  322. return showTopPage(req, res);
  323. };
  324. /**
  325. * Redirect to the page without trailing slash
  326. */
  327. actions.showPageWithEndOfSlash = function(req, res, next) {
  328. return res.redirect(pathUtils.removeTrailingSlash(req.path));
  329. };
  330. /**
  331. * switch action
  332. * - presentation mode
  333. * - by behaviorType
  334. */
  335. actions.showPage = async function(req, res, next) {
  336. // presentation mode
  337. if (req.query.presentation) {
  338. return showPageForPresentation(req, res, next);
  339. }
  340. // delegate to showPageForGrowiBehavior
  341. return showPageForGrowiBehavior(req, res, next);
  342. };
  343. actions.showSharedPage = async function(req, res, next) {
  344. const { linkId } = req.params;
  345. const revisionId = req.query.revision;
  346. const shareLink = await ShareLink.findOne({ _id: linkId }).populate('relatedPage');
  347. if (shareLink == null || shareLink.relatedPage == null) {
  348. // page or sharelink are not found
  349. return res.render('layout-growi/not_found_shared_page');
  350. }
  351. const renderVars = {};
  352. renderVars.sharelink = shareLink;
  353. // check if share link is expired
  354. if (shareLink.isExpired()) {
  355. // page is not found
  356. return res.render('layout-growi/expired_shared_page', renderVars);
  357. }
  358. let page = shareLink.relatedPage;
  359. // presentation mode
  360. if (req.query.presentation) {
  361. page = await page.populateDataToMakePresentation(revisionId);
  362. // populate
  363. addRenderVarsForPage(renderVars, page);
  364. return res.render('page_presentation', renderVars);
  365. }
  366. page.initLatestRevisionField(revisionId);
  367. // populate
  368. page = await page.populateDataToShowRevision();
  369. addRenderVarsForPage(renderVars, page);
  370. addRenderVarsForScope(renderVars, page);
  371. await interceptorManager.process('beforeRenderPage', req, res, renderVars);
  372. return res.render('layout-growi/shared_page', renderVars);
  373. };
  374. /**
  375. * switch action by behaviorType
  376. */
  377. /* eslint-disable no-else-return */
  378. actions.trashPageListShowWrapper = function(req, res) {
  379. // redirect to '/trash'
  380. return res.redirect('/trash');
  381. };
  382. /* eslint-enable no-else-return */
  383. /**
  384. * switch action by behaviorType
  385. */
  386. /* eslint-disable no-else-return */
  387. actions.trashPageShowWrapper = function(req, res) {
  388. // Crowi behavior for '/trash/*'
  389. return actions.deletedPageListShow(req, res);
  390. };
  391. /* eslint-enable no-else-return */
  392. /**
  393. * switch action by behaviorType
  394. */
  395. /* eslint-disable no-else-return */
  396. actions.deletedPageListShowWrapper = function(req, res) {
  397. const path = `/trash${getPathFromRequest(req)}`;
  398. return res.redirect(path);
  399. };
  400. /* eslint-enable no-else-return */
  401. actions.notFound = async function(req, res) {
  402. const path = getPathFromRequest(req);
  403. const isCreatable = Page.isCreatableName(path);
  404. let view;
  405. const renderVars = { path };
  406. if (!isCreatable) {
  407. view = 'layout-growi/not_creatable';
  408. }
  409. else if (req.isForbidden) {
  410. view = 'layout-growi/forbidden';
  411. }
  412. else {
  413. view = 'layout-growi/not_found';
  414. // retrieve templates
  415. if (req.user != null) {
  416. const template = await Page.findTemplate(path);
  417. if (template.templateBody) {
  418. const body = replacePlaceholdersOfTemplate(template.templateBody, req);
  419. const tags = template.templateTags;
  420. renderVars.template = body;
  421. renderVars.templateTags = tags;
  422. }
  423. }
  424. // add scope variables by ancestor page
  425. const ancestor = await Page.findAncestorByPathAndViewer(path, req.user);
  426. if (ancestor != null) {
  427. await ancestor.populate('grantedGroup').execPopulate();
  428. addRenderVarsForScope(renderVars, ancestor);
  429. }
  430. }
  431. const limit = 50;
  432. const offset = parseInt(req.query.offset) || 0;
  433. await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit, true);
  434. return res.render(view, renderVars);
  435. };
  436. actions.deletedPageListShow = async function(req, res) {
  437. // normalizePath makes '/trash/' -> '/trash'
  438. const path = pathUtils.normalizePath(`/trash${getPathFromRequest(req)}`);
  439. const limit = 50;
  440. const offset = parseInt(req.query.offset) || 0;
  441. const queryOptions = {
  442. offset,
  443. limit,
  444. includeTrashed: true,
  445. };
  446. const renderVars = {
  447. page: null,
  448. path,
  449. pages: [],
  450. };
  451. const result = await Page.findListWithDescendants(path, req.user, queryOptions);
  452. if (result.pages.length > limit) {
  453. result.pages.pop();
  454. }
  455. renderVars.pager = generatePager(result.offset, result.limit, result.totalCount);
  456. renderVars.pages = result.pages;
  457. res.render('layout-growi/page_list', renderVars);
  458. };
  459. /**
  460. * redirector
  461. */
  462. actions.redirector = async function(req, res) {
  463. const id = req.params.id;
  464. const page = await Page.findByIdAndViewer(id, req.user);
  465. if (page != null) {
  466. return res.redirect(encodeURI(page.path));
  467. }
  468. return res.redirect('/');
  469. };
  470. const api = {};
  471. actions.api = api;
  472. /**
  473. * @swagger
  474. *
  475. * /pages.list:
  476. * get:
  477. * tags: [Pages, CrowiCompatibles]
  478. * operationId: listPages
  479. * summary: /pages.list
  480. * description: Get list of pages
  481. * parameters:
  482. * - in: query
  483. * name: path
  484. * schema:
  485. * $ref: '#/components/schemas/Page/properties/path'
  486. * - in: query
  487. * name: user
  488. * schema:
  489. * $ref: '#/components/schemas/User/properties/username'
  490. * - in: query
  491. * name: limit
  492. * schema:
  493. * $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/limit'
  494. * - in: query
  495. * name: offset
  496. * schema:
  497. * $ref: '#/components/schemas/V1PaginateResult/properties/meta/properties/offset'
  498. * responses:
  499. * 200:
  500. * description: Succeeded to get list of pages.
  501. * content:
  502. * application/json:
  503. * schema:
  504. * properties:
  505. * ok:
  506. * $ref: '#/components/schemas/V1Response/properties/ok'
  507. * pages:
  508. * type: array
  509. * items:
  510. * $ref: '#/components/schemas/Page'
  511. * description: page list
  512. * 403:
  513. * $ref: '#/components/responses/403'
  514. * 500:
  515. * $ref: '#/components/responses/500'
  516. */
  517. /**
  518. * @api {get} /pages.list List pages by user
  519. * @apiName ListPage
  520. * @apiGroup Page
  521. *
  522. * @apiParam {String} path
  523. * @apiParam {String} user
  524. */
  525. api.list = async function(req, res) {
  526. const username = req.query.user || null;
  527. const path = req.query.path || null;
  528. const limit = +req.query.limit || 50;
  529. const offset = parseInt(req.query.offset) || 0;
  530. const queryOptions = { offset, limit: limit + 1 };
  531. // Accepts only one of these
  532. if (username === null && path === null) {
  533. return res.json(ApiResponse.error('Parameter user or path is required.'));
  534. }
  535. if (username !== null && path !== null) {
  536. return res.json(ApiResponse.error('Parameter user or path is required.'));
  537. }
  538. try {
  539. let result = null;
  540. if (path == null) {
  541. const user = await User.findUserByUsername(username);
  542. if (user === null) {
  543. throw new Error('The user not found.');
  544. }
  545. result = await Page.findListByCreator(user, req.user, queryOptions);
  546. }
  547. else {
  548. result = await Page.findListByStartWith(path, req.user, queryOptions);
  549. }
  550. if (result.pages.length > limit) {
  551. result.pages.pop();
  552. }
  553. return res.json(ApiResponse.success(result));
  554. }
  555. catch (err) {
  556. return res.json(ApiResponse.error(err));
  557. }
  558. };
  559. // TODO If everything that depends on this route, delete it too
  560. api.create = async function(req, res) {
  561. const body = req.body.body || null;
  562. let pagePath = req.body.path || null;
  563. const grant = req.body.grant || null;
  564. const grantUserGroupId = req.body.grantUserGroupId || null;
  565. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  566. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  567. const slackChannels = req.body.slackChannels || null;
  568. const socketClientId = req.body.socketClientId || undefined;
  569. const pageTags = req.body.pageTags || undefined;
  570. if (body === null || pagePath === null) {
  571. return res.json(ApiResponse.error('Parameters body and path are required.'));
  572. }
  573. // check whether path starts slash
  574. pagePath = pathUtils.addHeadingSlash(pagePath);
  575. // check page existence
  576. const isExist = await Page.count({ path: pagePath }) > 0;
  577. if (isExist) {
  578. return res.json(ApiResponse.error('Page exists', 'already_exists'));
  579. }
  580. const options = { socketClientId };
  581. if (grant != null) {
  582. options.grant = grant;
  583. options.grantUserGroupId = grantUserGroupId;
  584. }
  585. const createdPage = await Page.create(pagePath, body, req.user, options);
  586. let savedTags;
  587. if (pageTags != null) {
  588. await PageTagRelation.updatePageTags(createdPage.id, pageTags);
  589. savedTags = await PageTagRelation.listTagNamesByPage(createdPage.id);
  590. }
  591. const result = {
  592. page: serializePageSecurely(createdPage),
  593. revision: serializeRevisionSecurely(createdPage.revision),
  594. tags: savedTags,
  595. };
  596. res.json(ApiResponse.success(result));
  597. // update scopes for descendants
  598. if (overwriteScopesOfDescendants) {
  599. Page.applyScopesToDescendantsAsyncronously(createdPage, req.user);
  600. }
  601. // global notification
  602. try {
  603. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_CREATE, createdPage, req.user);
  604. }
  605. catch (err) {
  606. logger.error('Create notification failed', err);
  607. }
  608. // user notification
  609. if (isSlackEnabled) {
  610. try {
  611. const results = await userNotificationService.fire(createdPage, req.user, slackChannels, 'create');
  612. results.forEach((result) => {
  613. if (result.status === 'rejected') {
  614. logger.error('Create user notification failed', result.reason);
  615. }
  616. });
  617. }
  618. catch (err) {
  619. logger.error('Create user notification failed', err);
  620. }
  621. }
  622. };
  623. /**
  624. * @swagger
  625. *
  626. * /pages.update:
  627. * post:
  628. * tags: [Pages, CrowiCompatibles]
  629. * operationId: updatePage
  630. * summary: /pages.update
  631. * description: Update page
  632. * requestBody:
  633. * content:
  634. * application/json:
  635. * schema:
  636. * properties:
  637. * body:
  638. * $ref: '#/components/schemas/Revision/properties/body'
  639. * page_id:
  640. * $ref: '#/components/schemas/Page/properties/_id'
  641. * revision_id:
  642. * $ref: '#/components/schemas/Revision/properties/_id'
  643. * grant:
  644. * $ref: '#/components/schemas/Page/properties/grant'
  645. * required:
  646. * - body
  647. * - page_id
  648. * - revision_id
  649. * responses:
  650. * 200:
  651. * description: Succeeded to update page.
  652. * content:
  653. * application/json:
  654. * schema:
  655. * properties:
  656. * ok:
  657. * $ref: '#/components/schemas/V1Response/properties/ok'
  658. * page:
  659. * $ref: '#/components/schemas/Page'
  660. * revision:
  661. * $ref: '#/components/schemas/Revision'
  662. * 403:
  663. * $ref: '#/components/responses/403'
  664. * 500:
  665. * $ref: '#/components/responses/500'
  666. */
  667. /**
  668. * @api {post} /pages.update Update page
  669. * @apiName UpdatePage
  670. * @apiGroup Page
  671. *
  672. * @apiParam {String} body
  673. * @apiParam {String} page_id
  674. * @apiParam {String} revision_id
  675. * @apiParam {String} grant
  676. *
  677. * In the case of the page exists:
  678. * - If revision_id is specified => update the page,
  679. * - If revision_id is not specified => force update by the new contents.
  680. */
  681. api.update = async function(req, res) {
  682. const pageBody = req.body.body || null;
  683. const pageId = req.body.page_id || null;
  684. const revisionId = req.body.revision_id || null;
  685. const grant = req.body.grant || null;
  686. const grantUserGroupId = req.body.grantUserGroupId || null;
  687. const overwriteScopesOfDescendants = req.body.overwriteScopesOfDescendants || null;
  688. const isSlackEnabled = !!req.body.isSlackEnabled; // cast to boolean
  689. const slackChannels = req.body.slackChannels || null;
  690. const isSyncRevisionToHackmd = !!req.body.isSyncRevisionToHackmd; // cast to boolean
  691. const socketClientId = req.body.socketClientId || undefined;
  692. const pageTags = req.body.pageTags || undefined;
  693. if (pageId === null || pageBody === null || revisionId === null) {
  694. return res.json(ApiResponse.error('page_id, body and revision_id are required.'));
  695. }
  696. // check page existence
  697. const isExist = await Page.count({ _id: pageId }) > 0;
  698. if (!isExist) {
  699. return res.json(ApiResponse.error(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  700. }
  701. // check revision
  702. let page = await Page.findByIdAndViewer(pageId, req.user);
  703. if (page != null && revisionId != null && !page.isUpdatable(revisionId)) {
  704. return res.json(ApiResponse.error('Posted param "revisionId" is outdated.', 'outdated'));
  705. }
  706. const options = { isSyncRevisionToHackmd, socketClientId };
  707. if (grant != null) {
  708. options.grant = grant;
  709. options.grantUserGroupId = grantUserGroupId;
  710. }
  711. const Revision = crowi.model('Revision');
  712. const previousRevision = await Revision.findById(revisionId);
  713. try {
  714. page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
  715. }
  716. catch (err) {
  717. logger.error('error on _api/pages.update', err);
  718. return res.json(ApiResponse.error(err));
  719. }
  720. let savedTags;
  721. if (pageTags != null) {
  722. await PageTagRelation.updatePageTags(pageId, pageTags);
  723. savedTags = await PageTagRelation.listTagNamesByPage(pageId);
  724. }
  725. const result = {
  726. page: serializePageSecurely(page),
  727. revision: serializeRevisionSecurely(page.revision),
  728. tags: savedTags,
  729. };
  730. res.json(ApiResponse.success(result));
  731. // update scopes for descendants
  732. if (overwriteScopesOfDescendants) {
  733. Page.applyScopesToDescendantsAsyncronously(page, req.user);
  734. }
  735. // global notification
  736. try {
  737. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_EDIT, page, req.user);
  738. }
  739. catch (err) {
  740. logger.error('Edit notification failed', err);
  741. }
  742. // user notification
  743. if (isSlackEnabled) {
  744. try {
  745. const results = await userNotificationService.fire(page, req.user, slackChannels, 'update', { previousRevision });
  746. results.forEach((result) => {
  747. if (result.status === 'rejected') {
  748. logger.error('Create user notification failed', result.reason);
  749. }
  750. });
  751. }
  752. catch (err) {
  753. logger.error('Create user notification failed', err);
  754. }
  755. }
  756. };
  757. /**
  758. * @swagger
  759. *
  760. * /pages.get:
  761. * get:
  762. * tags: [Pages, CrowiCompatibles]
  763. * operationId: getPage
  764. * summary: /pages.get
  765. * description: Get page data
  766. * parameters:
  767. * - in: query
  768. * name: page_id
  769. * schema:
  770. * $ref: '#/components/schemas/Page/properties/_id'
  771. * - in: query
  772. * name: path
  773. * schema:
  774. * $ref: '#/components/schemas/Page/properties/path'
  775. * - in: query
  776. * name: revision_id
  777. * schema:
  778. * $ref: '#/components/schemas/Revision/properties/_id'
  779. * responses:
  780. * 200:
  781. * description: Succeeded to get page data.
  782. * content:
  783. * application/json:
  784. * schema:
  785. * properties:
  786. * ok:
  787. * $ref: '#/components/schemas/V1Response/properties/ok'
  788. * page:
  789. * $ref: '#/components/schemas/Page'
  790. * 403:
  791. * $ref: '#/components/responses/403'
  792. * 500:
  793. * $ref: '#/components/responses/500'
  794. */
  795. /**
  796. * @api {get} /pages.get Get page data
  797. * @apiName GetPage
  798. * @apiGroup Page
  799. *
  800. * @apiParam {String} page_id
  801. * @apiParam {String} path
  802. * @apiParam {String} revision_id
  803. */
  804. api.get = async function(req, res) {
  805. const pagePath = req.query.path || null;
  806. const pageId = req.query.page_id || null; // TODO: handling
  807. if (!pageId && !pagePath) {
  808. return res.json(ApiResponse.error(new Error('Parameter path or page_id is required.')));
  809. }
  810. let page;
  811. try {
  812. if (pageId) { // prioritized
  813. page = await Page.findByIdAndViewer(pageId, req.user);
  814. }
  815. else if (pagePath) {
  816. page = await Page.findByPathAndViewer(pagePath, req.user);
  817. }
  818. if (page == null) {
  819. throw new Error(`Page '${pageId || pagePath}' is not found or forbidden`, 'notfound_or_forbidden');
  820. }
  821. page.initLatestRevisionField();
  822. // populate
  823. page = await page.populateDataToShowRevision();
  824. }
  825. catch (err) {
  826. return res.json(ApiResponse.error(err));
  827. }
  828. const result = {};
  829. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  830. return res.json(ApiResponse.success(result));
  831. };
  832. /**
  833. * @swagger
  834. *
  835. * /pages.exist:
  836. * get:
  837. * tags: [Pages]
  838. * operationId: getPageExistence
  839. * summary: /pages.exist
  840. * description: Get page existence
  841. * parameters:
  842. * - in: query
  843. * name: pagePaths
  844. * schema:
  845. * type: string
  846. * description: Page path list in JSON Array format
  847. * example: '["/", "/user/unknown"]'
  848. * responses:
  849. * 200:
  850. * description: Succeeded to get page existence.
  851. * content:
  852. * application/json:
  853. * schema:
  854. * properties:
  855. * ok:
  856. * $ref: '#/components/schemas/V1Response/properties/ok'
  857. * pages:
  858. * type: string
  859. * description: Properties of page path and existence
  860. * example: '{"/": true, "/user/unknown": false}'
  861. * 403:
  862. * $ref: '#/components/responses/403'
  863. * 500:
  864. * $ref: '#/components/responses/500'
  865. */
  866. /**
  867. * @api {get} /pages.exist Get if page exists
  868. * @apiName GetPage
  869. * @apiGroup Page
  870. *
  871. * @apiParam {String} pages (stringified JSON)
  872. */
  873. api.exist = async function(req, res) {
  874. const pagePaths = JSON.parse(req.query.pagePaths || '[]');
  875. const pages = {};
  876. await Promise.all(pagePaths.map(async(path) => {
  877. // check page existence
  878. const isExist = await Page.count({ path }) > 0;
  879. pages[path] = isExist;
  880. return;
  881. }));
  882. const result = { pages };
  883. return res.json(ApiResponse.success(result));
  884. };
  885. /**
  886. * @swagger
  887. *
  888. * /pages.getPageTag:
  889. * get:
  890. * tags: [Pages]
  891. * operationId: getPageTag
  892. * summary: /pages.getPageTag
  893. * description: Get page tag
  894. * parameters:
  895. * - in: query
  896. * name: pageId
  897. * schema:
  898. * $ref: '#/components/schemas/Page/properties/_id'
  899. * responses:
  900. * 200:
  901. * description: Succeeded to get page tags.
  902. * content:
  903. * application/json:
  904. * schema:
  905. * properties:
  906. * ok:
  907. * $ref: '#/components/schemas/V1Response/properties/ok'
  908. * tags:
  909. * $ref: '#/components/schemas/Tags'
  910. * 403:
  911. * $ref: '#/components/responses/403'
  912. * 500:
  913. * $ref: '#/components/responses/500'
  914. */
  915. /**
  916. * @api {get} /pages.getPageTag get page tags
  917. * @apiName GetPageTag
  918. * @apiGroup Page
  919. *
  920. * @apiParam {String} pageId
  921. */
  922. api.getPageTag = async function(req, res) {
  923. const result = {};
  924. try {
  925. result.tags = await PageTagRelation.listTagNamesByPage(req.query.pageId);
  926. }
  927. catch (err) {
  928. return res.json(ApiResponse.error(err));
  929. }
  930. return res.json(ApiResponse.success(result));
  931. };
  932. /**
  933. * @swagger
  934. *
  935. * /pages.updatePost:
  936. * get:
  937. * tags: [Pages, CrowiCompatibles]
  938. * operationId: getUpdatePostPage
  939. * summary: /pages.updatePost
  940. * description: Get UpdatePost setting list
  941. * parameters:
  942. * - in: query
  943. * name: path
  944. * schema:
  945. * $ref: '#/components/schemas/Page/properties/path'
  946. * responses:
  947. * 200:
  948. * description: Succeeded to get UpdatePost setting list.
  949. * content:
  950. * application/json:
  951. * schema:
  952. * properties:
  953. * ok:
  954. * $ref: '#/components/schemas/V1Response/properties/ok'
  955. * updatePost:
  956. * $ref: '#/components/schemas/UpdatePost'
  957. * 403:
  958. * $ref: '#/components/responses/403'
  959. * 500:
  960. * $ref: '#/components/responses/500'
  961. */
  962. /**
  963. * @api {get} /pages.updatePost
  964. * @apiName Get UpdatePost setting list
  965. * @apiGroup Page
  966. *
  967. * @apiParam {String} path
  968. */
  969. api.getUpdatePost = function(req, res) {
  970. const path = req.query.path;
  971. const UpdatePost = crowi.model('UpdatePost');
  972. if (!path) {
  973. return res.json(ApiResponse.error({}));
  974. }
  975. UpdatePost.findSettingsByPath(path)
  976. .then((data) => {
  977. // eslint-disable-next-line no-param-reassign
  978. data = data.map((e) => {
  979. return e.channel;
  980. });
  981. debug('Found updatePost data', data);
  982. const result = { updatePost: data };
  983. return res.json(ApiResponse.success(result));
  984. })
  985. .catch((err) => {
  986. debug('Error occured while get setting', err);
  987. return res.json(ApiResponse.error({}));
  988. });
  989. };
  990. /**
  991. * @api {post} /pages.remove Remove page
  992. * @apiName RemovePage
  993. * @apiGroup Page
  994. *
  995. * @apiParam {String} page_id Page Id.
  996. * @apiParam {String} revision_id
  997. */
  998. api.remove = async function(req, res) {
  999. const pageId = req.body.page_id;
  1000. const previousRevision = req.body.revision_id || null;
  1001. const socketClientId = req.body.socketClientId || undefined;
  1002. // get completely flag
  1003. const isCompletely = (req.body.completely != null);
  1004. // get recursively flag
  1005. const isRecursively = (req.body.recursively != null);
  1006. const options = { socketClientId };
  1007. const page = await Page.findByIdAndViewer(pageId, req.user);
  1008. if (page == null) {
  1009. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  1010. }
  1011. debug('Delete page', page._id, page.path);
  1012. try {
  1013. if (isCompletely) {
  1014. if (!req.user.canDeleteCompletely(page.creator)) {
  1015. return res.json(ApiResponse.error('You can not delete completely', 'user_not_admin'));
  1016. }
  1017. await crowi.pageService.deleteCompletely(page, req.user, options, isRecursively);
  1018. }
  1019. else {
  1020. if (!page.isUpdatable(previousRevision)) {
  1021. return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
  1022. }
  1023. await crowi.pageService.deletePage(page, req.user, options, isRecursively);
  1024. }
  1025. }
  1026. catch (err) {
  1027. logger.error('Error occured while get setting', err);
  1028. return res.json(ApiResponse.error('Failed to delete page.', err.message));
  1029. }
  1030. debug('Page deleted', page.path);
  1031. const result = {};
  1032. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  1033. res.json(ApiResponse.success(result));
  1034. try {
  1035. // global notification
  1036. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_DELETE, page, req.user);
  1037. }
  1038. catch (err) {
  1039. logger.error('Delete notification failed', err);
  1040. }
  1041. };
  1042. /**
  1043. * @api {post} /pages.revertRemove Revert removed page
  1044. * @apiName RevertRemovePage
  1045. * @apiGroup Page
  1046. *
  1047. * @apiParam {String} page_id Page Id.
  1048. */
  1049. api.revertRemove = async function(req, res, options) {
  1050. const pageId = req.body.page_id;
  1051. const socketClientId = req.body.socketClientId || undefined;
  1052. // get recursively flag
  1053. const isRecursively = (req.body.recursively != null);
  1054. let page;
  1055. try {
  1056. page = await Page.findByIdAndViewer(pageId, req.user);
  1057. if (page == null) {
  1058. throw new Error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden');
  1059. }
  1060. page = await crowi.pageService.revertDeletedPage(page, req.user, { socketClientId }, isRecursively);
  1061. }
  1062. catch (err) {
  1063. logger.error('Error occured while get setting', err);
  1064. return res.json(ApiResponse.error('Failed to revert deleted page.'));
  1065. }
  1066. const result = {};
  1067. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  1068. return res.json(ApiResponse.success(result));
  1069. };
  1070. /**
  1071. * @swagger
  1072. *
  1073. * /pages.rename:
  1074. * post:
  1075. * tags: [Pages, CrowiCompatibles]
  1076. * operationId: renamePage
  1077. * summary: /pages.rename
  1078. * description: Rename page
  1079. * requestBody:
  1080. * content:
  1081. * application/json:
  1082. * schema:
  1083. * properties:
  1084. * page_id:
  1085. * $ref: '#/components/schemas/Page/properties/_id'
  1086. * path:
  1087. * $ref: '#/components/schemas/Page/properties/path'
  1088. * revision_id:
  1089. * $ref: '#/components/schemas/Revision/properties/_id'
  1090. * new_path:
  1091. * type: string
  1092. * description: new path
  1093. * example: /user/alice/new_test
  1094. * create_redirect:
  1095. * type: boolean
  1096. * description: whether redirect page
  1097. * required:
  1098. * - page_id
  1099. * responses:
  1100. * 200:
  1101. * description: Succeeded to rename page.
  1102. * content:
  1103. * application/json:
  1104. * schema:
  1105. * properties:
  1106. * ok:
  1107. * $ref: '#/components/schemas/V1Response/properties/ok'
  1108. * page:
  1109. * $ref: '#/components/schemas/Page'
  1110. * 403:
  1111. * $ref: '#/components/responses/403'
  1112. * 500:
  1113. * $ref: '#/components/responses/500'
  1114. */
  1115. /**
  1116. * @api {post} /pages.rename Rename page
  1117. * @apiName RenamePage
  1118. * @apiGroup Page
  1119. *
  1120. * @apiParam {String} page_id Page Id.
  1121. * @apiParam {String} path
  1122. * @apiParam {String} revision_id
  1123. * @apiParam {String} new_path New path name.
  1124. * @apiParam {Bool} create_redirect
  1125. */
  1126. api.rename = async function(req, res) {
  1127. const pageId = req.body.page_id;
  1128. const previousRevision = req.body.revision_id || null;
  1129. let newPagePath = pathUtils.normalizePath(req.body.new_path);
  1130. const options = {
  1131. createRedirectPage: (req.body.create_redirect != null),
  1132. updateMetadata: (req.body.remain_metadata == null),
  1133. socketClientId: +req.body.socketClientId || undefined,
  1134. };
  1135. const isRecursively = (req.body.recursively != null);
  1136. if (!Page.isCreatableName(newPagePath)) {
  1137. return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
  1138. }
  1139. // check whether path starts slash
  1140. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  1141. const isExist = await Page.count({ path: newPagePath }) > 0;
  1142. if (isExist) {
  1143. // if page found, cannot cannot rename to that path
  1144. return res.json(ApiResponse.error(`'new_path=${newPagePath}' already exists`, 'already_exists'));
  1145. }
  1146. let page;
  1147. try {
  1148. page = await Page.findByIdAndViewer(pageId, req.user);
  1149. if (page == null) {
  1150. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  1151. }
  1152. if (!page.isUpdatable(previousRevision)) {
  1153. return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
  1154. }
  1155. page = await crowi.pageService.renamePage(page, newPagePath, req.user, options, isRecursively);
  1156. }
  1157. catch (err) {
  1158. logger.error(err);
  1159. return res.json(ApiResponse.error('Failed to update page.', 'unknown'));
  1160. }
  1161. const result = {};
  1162. result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
  1163. res.json(ApiResponse.success(result));
  1164. try {
  1165. // global notification
  1166. await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
  1167. oldPath: req.body.path,
  1168. });
  1169. }
  1170. catch (err) {
  1171. logger.error('Move notification failed', err);
  1172. }
  1173. return page;
  1174. };
  1175. /**
  1176. * @swagger
  1177. *
  1178. * /pages.duplicate:
  1179. * post:
  1180. * tags: [Pages]
  1181. * operationId: duplicatePage
  1182. * summary: /pages.duplicate
  1183. * description: Duplicate page
  1184. * requestBody:
  1185. * content:
  1186. * application/json:
  1187. * schema:
  1188. * properties:
  1189. * page_id:
  1190. * $ref: '#/components/schemas/Page/properties/_id'
  1191. * new_path:
  1192. * $ref: '#/components/schemas/Page/properties/path'
  1193. * required:
  1194. * - page_id
  1195. * responses:
  1196. * 200:
  1197. * description: Succeeded to duplicate page.
  1198. * content:
  1199. * application/json:
  1200. * schema:
  1201. * properties:
  1202. * ok:
  1203. * $ref: '#/components/schemas/V1Response/properties/ok'
  1204. * page:
  1205. * $ref: '#/components/schemas/Page'
  1206. * tags:
  1207. * $ref: '#/components/schemas/Tags'
  1208. * 403:
  1209. * $ref: '#/components/responses/403'
  1210. * 500:
  1211. * $ref: '#/components/responses/500'
  1212. */
  1213. /**
  1214. * @api {post} /pages.duplicate Duplicate page
  1215. * @apiName DuplicatePage
  1216. * @apiGroup Page
  1217. *
  1218. * @apiParam {String} page_id Page Id.
  1219. * @apiParam {String} new_path New path name.
  1220. */
  1221. api.duplicate = async function(req, res) {
  1222. const pageId = req.body.page_id;
  1223. let newPagePath = pathUtils.normalizePath(req.body.new_path);
  1224. const page = await Page.findByIdAndViewer(pageId, req.user);
  1225. if (page == null) {
  1226. return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
  1227. }
  1228. // check whether path starts slash
  1229. newPagePath = pathUtils.addHeadingSlash(newPagePath);
  1230. await page.populateDataToShowRevision();
  1231. const originTags = await page.findRelatedTagsById();
  1232. req.body.path = newPagePath;
  1233. req.body.body = page.revision.body;
  1234. req.body.grant = page.grant;
  1235. req.body.grantedUsers = page.grantedUsers;
  1236. req.body.grantUserGroupId = page.grantedGroup;
  1237. req.body.pageTags = originTags;
  1238. return api.create(req, res);
  1239. };
  1240. /**
  1241. * @api {post} /pages.unlink Remove the redirecting page
  1242. * @apiName UnlinkPage
  1243. * @apiGroup Page
  1244. *
  1245. * @apiParam {String} page_id Page Id.
  1246. * @apiParam {String} revision_id
  1247. */
  1248. api.unlink = async function(req, res) {
  1249. const path = req.body.path;
  1250. try {
  1251. await Page.removeRedirectOriginPageByPath(path);
  1252. logger.debug('Redirect Page deleted', path);
  1253. }
  1254. catch (err) {
  1255. logger.error('Error occured while get setting', err);
  1256. return res.json(ApiResponse.error('Failed to delete redirect page.'));
  1257. }
  1258. const result = { path };
  1259. return res.json(ApiResponse.success(result));
  1260. };
  1261. return actions;
  1262. };