| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826 |
- import { pagePathUtils, AllSubscriptionStatusType, SubscriptionStatusType } from '@growi/core';
- import { SupportedAction, SupportedTargetModel } from '~/interfaces/activity';
- import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
- import Subscription from '~/server/models/subscription';
- import UserGroup from '~/server/models/user-group';
- import loggerFactory from '~/utils/logger';
- import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
- const logger = loggerFactory('growi:routes:apiv3:page'); // eslint-disable-line no-unused-vars
- const express = require('express');
- const { body, query, param } = require('express-validator');
- const router = express.Router();
- const { convertToNewAffiliationPath, isTopPage } = pagePathUtils;
- const ErrorV3 = require('../../models/vo/error-apiv3');
- /**
- * @swagger
- * tags:
- * name: Page
- */
- /**
- * @swagger
- *
- * components:
- * schemas:
- * Page:
- * description: Page
- * type: object
- * properties:
- * _id:
- * type: string
- * description: page ID
- * example: 5e07345972560e001761fa63
- * __v:
- * type: number
- * description: DB record version
- * example: 0
- * commentCount:
- * type: number
- * description: count of comments
- * example: 3
- * createdAt:
- * type: string
- * description: date created at
- * example: 2010-01-01T00:00:00.000Z
- * creator:
- * $ref: '#/components/schemas/User'
- * extended:
- * type: object
- * description: extend data
- * example: {}
- * grant:
- * type: number
- * description: grant
- * example: 1
- * grantedUsers:
- * type: array
- * description: granted users
- * items:
- * type: string
- * description: user ID
- * example: ["5ae5fccfc5577b0004dbd8ab"]
- * lastUpdateUser:
- * $ref: '#/components/schemas/User'
- * liker:
- * type: array
- * description: granted users
- * items:
- * type: string
- * description: user ID
- * example: []
- * path:
- * type: string
- * description: page path
- * example: /
- * revision:
- * type: string
- * description: page revision
- * seenUsers:
- * type: array
- * description: granted users
- * items:
- * type: string
- * description: user ID
- * example: ["5ae5fccfc5577b0004dbd8ab"]
- * status:
- * type: string
- * description: status
- * enum:
- * - 'wip'
- * - 'published'
- * - 'deleted'
- * - 'deprecated'
- * example: published
- * updatedAt:
- * type: string
- * description: date updated at
- * example: 2010-01-01T00:00:00.000Z
- *
- * LikeParams:
- * description: LikeParams
- * type: object
- * properties:
- * pageId:
- * type: string
- * description: page ID
- * example: 5e07345972560e001761fa63
- * bool:
- * type: boolean
- * description: boolean for like status
- *
- * PageInfo:
- * description: PageInfo
- * type: object
- * required:
- * - sumOfLikers
- * - likerIds
- * - sumOfSeenUsers
- * - seenUserIds
- * properties:
- * isLiked:
- * type: boolean
- * description: Whether the page is liked by the logged in user
- * sumOfLikers:
- * type: number
- * description: Number of users who have liked the page
- * likerIds:
- * type: array
- * items:
- * type: string
- * description: Ids of users who have liked the page
- * example: ["5e07345972560e001761fa63"]
- * sumOfSeenUsers:
- * type: number
- * description: Number of users who have seen the page
- * seenUserIds:
- * type: array
- * items:
- * type: string
- * description: Ids of users who have seen the page
- * example: ["5e07345972560e001761fa63"]
- *
- * PageParams:
- * description: PageParams
- * type: object
- * required:
- * - pageId
- * properties:
- * pageId:
- * type: string
- * description: page ID
- * example: 5e07345972560e001761fa63
- */
- module.exports = (crowi) => {
- const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
- const loginRequired = require('../../middlewares/login-required')(crowi, true);
- const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
- const certifySharedPage = require('../../middlewares/certify-shared-page')(crowi);
- const addActivity = generateAddActivityMiddleware(crowi);
- const globalNotificationService = crowi.getGlobalNotificationService();
- const socketIoService = crowi.socketIoService;
- const { Page, GlobalNotificationSetting, Bookmark } = crowi.models;
- const { pageService, exportService } = crowi;
- const activityEvent = crowi.event('activity');
- const validator = {
- getPage: [
- query('pageId').optional().isString(),
- query('path').optional().isString(),
- query('findAll').optional().isBoolean(),
- ],
- likes: [
- body('pageId').isString(),
- body('bool').isBoolean(),
- ],
- info: [
- query('pageId').isMongoId().withMessage('pageId is required'),
- ],
- isGrantNormalized: [
- query('pageId').isMongoId().withMessage('pageId is required'),
- ],
- applicableGrant: [
- query('pageId').isMongoId().withMessage('pageId is required'),
- ],
- updateGrant: [
- param('pageId').isMongoId().withMessage('pageId is required'),
- body('grant').isInt().withMessage('grant is required'),
- body('grantedGroup').optional().isMongoId().withMessage('grantedGroup must be a mongo id'),
- ],
- export: [
- query('format').isString().isIn(['md', 'pdf']),
- query('revisionId').isString(),
- ],
- archive: [
- body('rootPagePath').isString(),
- body('isCommentDownload').isBoolean(),
- body('isAttachmentFileDownload').isBoolean(),
- body('isSubordinatedPageDownload').isBoolean(),
- body('fileType').isString().isIn(['pdf', 'markdown']),
- body('hierarchyType').isString().isIn(['allSubordinatedPage', 'decideHierarchy']),
- body('hierarchyValue').isNumeric(),
- ],
- exist: [
- query('fromPath').isString(),
- query('toPath').isString(),
- ],
- subscribe: [
- body('pageId').isString(),
- body('status').isIn(AllSubscriptionStatusType),
- ],
- subscribeStatus: [
- query('pageId').isString(),
- ],
- };
- /**
- * @swagger
- *
- * /page:
- * get:
- * tags: [Page]
- * operationId: getPage
- * summary: /page
- * description: get page by pagePath or pageId
- * parameters:
- * - name: pageId
- * in: query
- * description: page id
- * schema:
- * $ref: '#/components/schemas/Page/properties/_id'
- * - name: path
- * in: query
- * description: page path
- * schema:
- * $ref: '#/components/schemas/Page/properties/path'
- * responses:
- * 200:
- * description: Page data
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/Page'
- */
- router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
- const { user } = req;
- const { pageId, path, findAll } = req.query;
- if (pageId == null && path == null) {
- return res.apiv3Err(new ErrorV3('Either parameter of path or pageId is required.', 'invalid-request'));
- }
- let page;
- let pages;
- try {
- if (pageId != null) { // prioritized
- page = await Page.findByIdAndViewer(pageId, user);
- }
- else if (!findAll) {
- page = await Page.findByPathAndViewer(path, user, null, true);
- }
- else {
- pages = await Page.findByPathAndViewer(path, user, null, false);
- }
- }
- catch (err) {
- logger.error('get-page-failed', err);
- return res.apiv3Err(err, 500);
- }
- if (page == null && (pages == null || pages.length === 0)) {
- return res.apiv3Err('Page is not found', 404);
- }
- if (page != null) {
- try {
- page.initLatestRevisionField();
- // populate
- page = await page.populateDataToShowRevision();
- }
- catch (err) {
- logger.error('populate-page-failed', err);
- return res.apiv3Err(err, 500);
- }
- }
- return res.apiv3({ page, pages });
- });
- /**
- * @swagger
- *
- * /page/likes:
- * put:
- * tags: [Page]
- * summary: /page/likes
- * description: Update liked status
- * operationId: updateLikedStatus
- * requestBody:
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/LikeParams'
- * responses:
- * 200:
- * description: Succeeded to update liked status.
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/Page'
- */
- router.put('/likes', accessTokenParser, loginRequiredStrictly, addActivity, validator.likes, apiV3FormValidator, async(req, res) => {
- const { pageId, bool: isLiked } = req.body;
- let page;
- try {
- page = await Page.findByIdAndViewer(pageId, req.user);
- if (page == null) {
- return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
- }
- if (isLiked) {
- page = await page.like(req.user);
- }
- else {
- page = await page.unlike(req.user);
- }
- }
- catch (err) {
- logger.error('update-like-failed', err);
- return res.apiv3Err(err, 500);
- }
- const result = { page };
- result.seenUser = page.seenUsers;
- const parameters = {
- targetModel: SupportedTargetModel.MODEL_PAGE,
- target: page,
- action: isLiked ? SupportedAction.ACTION_PAGE_LIKE : SupportedAction.ACTION_PAGE_UNLIKE,
- };
- activityEvent.emit('update', res.locals.activity._id, parameters, page);
- res.apiv3({ result });
- if (isLiked) {
- try {
- // global notification
- await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_LIKE, page, req.user);
- }
- catch (err) {
- logger.error('Like notification failed', err);
- }
- }
- });
- /**
- * @swagger
- *
- * /page/info:
- * get:
- * tags: [Page]
- * summary: /page/info
- * description: Retrieve current page info
- * operationId: getPageInfo
- * requestBody:
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/PageParams'
- * responses:
- * 200:
- * description: Successfully retrieved current page info.
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/PageInfo'
- * 500:
- * description: Internal server error.
- */
- router.get('/info', certifySharedPage, loginRequired, validator.info, apiV3FormValidator, async(req, res) => {
- const { user, isSharedPage } = req;
- const { pageId } = req.query;
- try {
- const pageWithMeta = await pageService.findPageAndMetaDataByViewer(pageId, null, user, true, isSharedPage);
- if (pageWithMeta == null) {
- return res.apiv3Err(`Page '${pageId}' is not found or forbidden`);
- }
- return res.apiv3(pageWithMeta.meta);
- }
- catch (err) {
- logger.error('get-page-info', err);
- return res.apiv3Err(err, 500);
- }
- });
- /**
- * @swagger
- *
- * /page/is-grant-normalized:
- * get:
- * tags: [Page]
- * summary: /page/info
- * description: Retrieve current page's isGrantNormalized value
- * operationId: getIsGrantNormalized
- * parameters:
- * - name: pageId
- * in: query
- * description: page id
- * schema:
- * $ref: '#/components/schemas/Page/properties/_id'
- * responses:
- * 200:
- * description: Successfully retrieved current isGrantNormalized.
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * isGrantNormalized:
- * type: boolean
- * 400:
- * description: Bad request. Page is unreachable or empty.
- * 500:
- * description: Internal server error.
- */
- router.get('/is-grant-normalized', loginRequiredStrictly, validator.isGrantNormalized, apiV3FormValidator, async(req, res) => {
- const { pageId } = req.query;
- const Page = crowi.model('Page');
- const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
- if (page == null) {
- return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
- }
- const {
- path, grant, grantedUsers, grantedGroup,
- } = page;
- let isGrantNormalized;
- try {
- isGrantNormalized = await crowi.pageGrantService.isGrantNormalized(req.user, path, grant, grantedUsers, grantedGroup, false, false);
- }
- catch (err) {
- logger.error('Error occurred while processing isGrantNormalized.', err);
- return res.apiv3Err(err, 500);
- }
- const currentPageUserGroup = await UserGroup.findOne({ _id: grantedGroup });
- const currentPageGrant = {
- grant,
- grantedGroup: currentPageUserGroup != null
- ? {
- id: currentPageUserGroup._id,
- name: currentPageUserGroup.name,
- }
- : null,
- };
- // page doesn't have parent page
- if (page.parent == null) {
- const grantData = {
- isForbidden: false,
- currentPageGrant,
- parentPageGrant: null,
- };
- return res.apiv3({ isGrantNormalized, grantData });
- }
- const parentPage = await Page.findByIdAndViewer(page.parent, req.user, null, false);
- // user isn't allowed to see parent's grant
- if (parentPage == null) {
- const grantData = {
- isForbidden: true,
- currentPageGrant,
- parentPageGrant: null,
- };
- return res.apiv3({ isGrantNormalized, grantData });
- }
- const parentPageUserGroup = await UserGroup.findOne({ _id: parentPage.grantedGroup });
- const parentPageGrant = {
- grant: parentPage.grant,
- grantedGroup: parentPageUserGroup != null
- ? {
- id: parentPageUserGroup._id,
- name: parentPageUserGroup.name,
- }
- : null,
- };
- const grantData = {
- isForbidden: false,
- currentPageGrant,
- parentPageGrant,
- };
- return res.apiv3({ isGrantNormalized, grantData });
- });
- router.get('/applicable-grant', loginRequiredStrictly, validator.applicableGrant, apiV3FormValidator, async(req, res) => {
- const { pageId } = req.query;
- const Page = crowi.model('Page');
- const page = await Page.findByIdAndViewer(pageId, req.user, null);
- if (page == null) {
- return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
- }
- let data;
- try {
- data = await crowi.pageGrantService.calcApplicableGrantData(page, req.user);
- }
- catch (err) {
- logger.error('Error occurred while processing calcApplicableGrantData.', err);
- return res.apiv3Err(err, 500);
- }
- return res.apiv3(data);
- });
- router.put('/:pageId/grant', loginRequiredStrictly, validator.updateGrant, apiV3FormValidator, async(req, res) => {
- const { pageId } = req.params;
- const { grant, grantedGroup } = req.body;
- const Page = crowi.model('Page');
- const page = await Page.findByIdAndViewer(pageId, req.user, null, false);
- if (page == null) {
- return res.apiv3Err(new ErrorV3('Page is unreachable or empty.', 'page_unreachable_or_empty'), 400);
- }
- let data;
- try {
- const shouldUseV4Process = false;
- const grantData = { grant, grantedGroup };
- data = await Page.updateGrant(page, req.user, grantData, shouldUseV4Process);
- }
- catch (err) {
- logger.error('Error occurred while processing calcApplicableGrantData.', err);
- return res.apiv3Err(err, 500);
- }
- return res.apiv3(data);
- });
- /**
- * @swagger
- *
- * /pages/export:
- * get:
- * tags: [Export]
- * description: return page's markdown
- * responses:
- * 200:
- * description: Return page's markdown
- */
- router.get('/export/:pageId', loginRequiredStrictly, validator.export, async(req, res) => {
- const { pageId } = req.params;
- const { format, revisionId = null } = req.query;
- let revision;
- try {
- const Page = crowi.model('Page');
- const page = await Page.findByIdAndViewer(pageId, req.user);
- if (page == null) {
- const isPageExist = await Page.count({ _id: pageId }) > 0;
- if (isPageExist) {
- // This page exists but req.user has not read permission
- return res.apiv3Err(new ErrorV3(`Haven't the right to see the page ${pageId}.`), 403);
- }
- return res.apiv3Err(new ErrorV3(`Page ${pageId} is not exist.`), 404);
- }
- const revisionIdForFind = revisionId || page.revision;
- const Revision = crowi.model('Revision');
- revision = await Revision.findById(revisionIdForFind);
- }
- catch (err) {
- logger.error('Failed to get page data', err);
- return res.apiv3Err(err, 500);
- }
- const fileName = revision.id;
- let stream;
- try {
- stream = exportService.getReadStreamFromRevision(revision, format);
- }
- catch (err) {
- logger.error('Failed to create readStream', err);
- return res.apiv3Err(err, 500);
- }
- res.set({
- 'Content-Disposition': `attachment;filename*=UTF-8''${fileName}.${format}`,
- });
- const parameters = {
- ip: req.ip,
- endpoint: req.originalUrl,
- action: SupportedAction.ACTION_PAGE_EXPORT,
- user: req.user?._id,
- snapshot: {
- username: req.user?.username,
- },
- };
- await crowi.activityService.createActivity(parameters);
- return stream.pipe(res);
- });
- /**
- * @swagger
- *
- * /page/exist-paths:
- * get:
- * tags: [Page]
- * summary: /page/exist-paths
- * description: Get already exist paths
- * operationId: getAlreadyExistPaths
- * parameters:
- * - name: fromPath
- * in: query
- * description: old parent path
- * schema:
- * type: string
- * - name: toPath
- * in: query
- * description: new parent path
- * schema:
- * type: string
- * responses:
- * 200:
- * description: Succeeded to retrieve pages.
- * content:
- * application/json:
- * schema:
- * properties:
- * existPaths:
- * type: object
- * description: Paths are already exist in DB
- * 500:
- * description: Internal server error.
- */
- router.get('/exist-paths', loginRequired, validator.exist, apiV3FormValidator, async(req, res) => {
- const { fromPath, toPath } = req.query;
- try {
- const fromPage = await Page.findByPath(fromPath, true);
- if (fromPage == null) {
- return res.apiv3Err(new ErrorV3('fromPage is Null'), 400);
- }
- const fromPageDescendants = await Page.findManageableListWithDescendants(fromPage, req.user, {}, true);
- const toPathDescendantsArray = fromPageDescendants.map((subordinatedPage) => {
- return convertToNewAffiliationPath(fromPath, toPath, subordinatedPage.path);
- });
- const existPages = await Page.findListByPathsArray(toPathDescendantsArray);
- const existPaths = existPages.map(page => page.path);
- return res.apiv3({ existPaths });
- }
- catch (err) {
- logger.error('Failed to get exist path', err);
- return res.apiv3Err(err, 500);
- }
- });
- // TODO GW-2746 bulk export pages
- // /**
- // * @swagger
- // *
- // * /page/archive:
- // * post:
- // * tags: [Page]
- // * summary: /page/archive
- // * description: create page archive
- // * requestBody:
- // * content:
- // * application/json:
- // * schema:
- // * properties:
- // * rootPagePath:
- // * type: string
- // * description: path of the root page
- // * isCommentDownload:
- // * type: boolean
- // * description: whether archive data contains comments
- // * isAttachmentFileDownload:
- // * type: boolean
- // * description: whether archive data contains attachments
- // * isSubordinatedPageDownload:
- // * type: boolean
- // * description: whether archive data children pages
- // * fileType:
- // * type: string
- // * description: file type of archive data(.md, .pdf)
- // * hierarchyType:
- // * type: string
- // * description: method of select children pages archive data contains('allSubordinatedPage', 'decideHierarchy')
- // * hierarchyValue:
- // * type: number
- // * description: depth of hierarchy(use when hierarchyType is 'decideHierarchy')
- // * responses:
- // * 200:
- // * description: create page archive
- // * content:
- // * application/json:
- // * schema:
- // * $ref: '#/components/schemas/Page'
- // */
- // router.post('/archive', accessTokenParser, loginRequired, validator.archive, apiV3FormValidator, async(req, res) => {
- // const PageArchive = crowi.model('PageArchive');
- // const {
- // rootPagePath,
- // isCommentDownload,
- // isAttachmentFileDownload,
- // fileType,
- // } = req.body;
- // const owner = req.user._id;
- // const numOfPages = 1; // TODO 最終的にzipファイルに取り込むページ数を入れる
- // const createdPageArchive = PageArchive.create({
- // owner,
- // fileType,
- // rootPagePath,
- // numOfPages,
- // hasComment: isCommentDownload,
- // hasAttachment: isAttachmentFileDownload,
- // });
- // console.log(createdPageArchive);
- // return res.apiv3({ });
- // });
- // router.get('/count-children-pages', accessTokenParser, loginRequired, async(req, res) => {
- // // TO DO implement correct number at another task
- // const { pageId } = req.query;
- // console.log(pageId);
- // const dummy = 6;
- // return res.apiv3({ dummy });
- // });
- /**
- * @swagger
- *
- * /page/subscribe:
- * put:
- * tags: [Page]
- * summary: /page/subscribe
- * description: Update subscription status
- * operationId: updateSubscriptionStatus
- * requestBody:
- * content:
- * application/json:
- * schema:
- * properties:
- * pageId:
- * $ref: '#/components/schemas/Page/properties/_id'
- * responses:
- * 200:
- * description: Succeeded to update subscription status.
- * content:
- * application/json:
- * schema:
- * $ref: '#/components/schemas/Page'
- * 500:
- * description: Internal server error.
- */
- router.put('/subscribe', accessTokenParser, loginRequiredStrictly, addActivity, validator.subscribe, apiV3FormValidator, async(req, res) => {
- const { pageId, status } = req.body;
- const userId = req.user._id;
- try {
- const subscription = await Subscription.subscribeByPageId(userId, pageId, status);
- const parameters = {};
- if (SubscriptionStatusType.SUBSCRIBE === status) {
- Object.assign(parameters, { action: SupportedAction.ACTION_PAGE_SUBSCRIBE });
- }
- else if (SubscriptionStatusType.UNSUBSCRIBE === status) {
- Object.assign(parameters, { action: SupportedAction.ACTION_PAGE_UNSUBSCRIBE });
- }
- if ('action' in parameters) {
- activityEvent.emit('update', res.locals.activity._id, parameters);
- }
- return res.apiv3({ subscription });
- }
- catch (err) {
- logger.error('Failed to update subscribe status', err);
- return res.apiv3Err(err, 500);
- }
- });
- return router;
- };
|