|
@@ -1,34 +1,39 @@
|
|
|
|
|
+import pathlib from 'path';
|
|
|
|
|
+import { Readable, Writable } from 'stream';
|
|
|
|
|
+
|
|
|
import { pagePathUtils, pathUtils } from '@growi/core';
|
|
import { pagePathUtils, pathUtils } from '@growi/core';
|
|
|
-import mongoose, { ObjectId, QueryCursor } from 'mongoose';
|
|
|
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
|
|
|
|
+import mongoose, { ObjectId, QueryCursor } from 'mongoose';
|
|
|
import streamToPromise from 'stream-to-promise';
|
|
import streamToPromise from 'stream-to-promise';
|
|
|
-import pathlib from 'path';
|
|
|
|
|
-import { Readable, Writable } from 'stream';
|
|
|
|
|
|
|
|
|
|
-import { createBatchStream } from '~/server/util/batch-stream';
|
|
|
|
|
-import loggerFactory from '~/utils/logger';
|
|
|
|
|
-import {
|
|
|
|
|
- CreateMethod, PageCreateOptions, PageModel, PageDocument,
|
|
|
|
|
-} from '~/server/models/page';
|
|
|
|
|
-import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
|
|
|
|
|
|
|
+import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
|
|
|
|
|
+import { Ref } from '~/interfaces/common';
|
|
|
|
|
+import { HasObjectId } from '~/interfaces/has-object-id';
|
|
|
import {
|
|
import {
|
|
|
IPage, IPageInfo, IPageInfoForEntity, IPageWithMeta,
|
|
IPage, IPageInfo, IPageInfoForEntity, IPageWithMeta,
|
|
|
} from '~/interfaces/page';
|
|
} from '~/interfaces/page';
|
|
|
-import { serializePageSecurely } from '../models/serializers/page-serializer';
|
|
|
|
|
-import { PageRedirectModel } from '../models/page-redirect';
|
|
|
|
|
-import Subscription from '../models/subscription';
|
|
|
|
|
-import { ObjectIdLike } from '../interfaces/mongoose-utils';
|
|
|
|
|
-import { IUserHasId } from '~/interfaces/user';
|
|
|
|
|
-import { Ref } from '~/interfaces/common';
|
|
|
|
|
-import { HasObjectId } from '~/interfaces/has-object-id';
|
|
|
|
|
-import { SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
|
|
|
|
|
import {
|
|
import {
|
|
|
PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
|
|
PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
|
|
|
} from '~/interfaces/page-delete-config';
|
|
} from '~/interfaces/page-delete-config';
|
|
|
-import PageOperation, { PageActionStage, PageActionType } from '../models/page-operation';
|
|
|
|
|
-import ActivityDefine from '../util/activityDefine';
|
|
|
|
|
|
|
+import { IUserHasId } from '~/interfaces/user';
|
|
|
|
|
+import { SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
|
|
|
|
|
+import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
|
|
|
|
|
+import {
|
|
|
|
|
+ CreateMethod, PageCreateOptions, PageModel, PageDocument,
|
|
|
|
|
+} from '~/server/models/page';
|
|
|
|
|
+import { createBatchStream } from '~/server/util/batch-stream';
|
|
|
|
|
+import loggerFactory from '~/utils/logger';
|
|
|
import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
|
|
import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
|
|
|
|
|
|
|
|
|
|
+import { ObjectIdLike } from '../interfaces/mongoose-utils';
|
|
|
|
|
+import { PathAlreadyExistsError } from '../models/errors';
|
|
|
|
|
+import PageOperation, { PageActionStage, PageActionType } from '../models/page-operation';
|
|
|
|
|
+import { PageRedirectModel } from '../models/page-redirect';
|
|
|
|
|
+import { serializePageSecurely } from '../models/serializers/page-serializer';
|
|
|
|
|
+import Subscription from '../models/subscription';
|
|
|
|
|
+import { V5ConversionError } from '../models/vo/v5-conversion-error';
|
|
|
|
|
+import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
|
|
|
|
|
+
|
|
|
const debug = require('debug')('growi:services:page');
|
|
const debug = require('debug')('growi:services:page');
|
|
|
|
|
|
|
|
const logger = loggerFactory('growi:services:page');
|
|
const logger = loggerFactory('growi:services:page');
|
|
@@ -154,7 +159,7 @@ class PageService {
|
|
|
this.pageEvent.onUpdate();
|
|
this.pageEvent.onUpdate();
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_UPDATE);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_UPDATE);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -164,7 +169,17 @@ class PageService {
|
|
|
// rename
|
|
// rename
|
|
|
this.pageEvent.on('rename', async(page, user) => {
|
|
this.pageEvent.on('rename', async(page, user) => {
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_RENAME);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_RENAME);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ logger.error(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // duplicate
|
|
|
|
|
+ this.pageEvent.on('duplicate', async(page, user) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DUPLICATE);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -174,7 +189,7 @@ class PageService {
|
|
|
// delete
|
|
// delete
|
|
|
this.pageEvent.on('delete', async(page, user) => {
|
|
this.pageEvent.on('delete', async(page, user) => {
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -184,7 +199,17 @@ class PageService {
|
|
|
// delete completely
|
|
// delete completely
|
|
|
this.pageEvent.on('deleteCompletely', async(page, user) => {
|
|
this.pageEvent.on('deleteCompletely', async(page, user) => {
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_DELETE_COMPLETELY);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_DELETE_COMPLETELY);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ logger.error(err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // revert
|
|
|
|
|
+ this.pageEvent.on('revert', async(page, user) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_REVERT);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -194,7 +219,7 @@ class PageService {
|
|
|
// likes
|
|
// likes
|
|
|
this.pageEvent.on('like', async(page, user) => {
|
|
this.pageEvent.on('like', async(page, user) => {
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_LIKE);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_LIKE);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -204,7 +229,7 @@ class PageService {
|
|
|
// bookmark
|
|
// bookmark
|
|
|
this.pageEvent.on('bookmark', async(page, user) => {
|
|
this.pageEvent.on('bookmark', async(page, user) => {
|
|
|
try {
|
|
try {
|
|
|
- await this.createAndSendNotifications(page, user, ActivityDefine.ACTION_PAGE_BOOKMARK);
|
|
|
|
|
|
|
+ await this.createAndSendNotifications(page, user, SUPPORTED_ACTION_TYPE.ACTION_PAGE_BOOKMARK);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|
|
|
logger.error(err);
|
|
logger.error(err);
|
|
@@ -963,6 +988,7 @@ class PageService {
|
|
|
newPagePath, page.revision.body, user, options,
|
|
newPagePath, page.revision.body, user, options,
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+ this.pageEvent.emit('duplicate', page, user);
|
|
|
|
|
|
|
|
// 4. Take over tags
|
|
// 4. Take over tags
|
|
|
const originTags = await page.findRelatedTagsById();
|
|
const originTags = await page.findRelatedTagsById();
|
|
@@ -1057,6 +1083,7 @@ class PageService {
|
|
|
const createdPage = await Page.create(
|
|
const createdPage = await Page.create(
|
|
|
newPagePath, page.revision.body, user, options,
|
|
newPagePath, page.revision.body, user, options,
|
|
|
);
|
|
);
|
|
|
|
|
+ this.pageEvent.emit('duplicate', page, user);
|
|
|
|
|
|
|
|
if (isRecursively) {
|
|
if (isRecursively) {
|
|
|
this.duplicateDescendantsWithStream(page, newPagePath, user);
|
|
this.duplicateDescendantsWithStream(page, newPagePath, user);
|
|
@@ -1879,7 +1906,7 @@ class PageService {
|
|
|
|
|
|
|
|
// throw if any page already exists
|
|
// throw if any page already exists
|
|
|
if (originPage != null) {
|
|
if (originPage != null) {
|
|
|
- throw Error(`This page cannot be reverted since a page with path "${originPage.path}" already exists. Rename the existing pages first.`);
|
|
|
|
|
|
|
+ throw new PathAlreadyExistsError('already_exists', originPage.path);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 2. Revert target
|
|
// 2. Revert target
|
|
@@ -1891,6 +1918,8 @@ class PageService {
|
|
|
}, { new: true });
|
|
}, { new: true });
|
|
|
await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
|
|
await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
|
|
|
|
|
|
|
|
|
|
+ this.pageEvent.emit('revert', page, user);
|
|
|
|
|
+
|
|
|
if (!isRecursively) {
|
|
if (!isRecursively) {
|
|
|
await this.updateDescendantCountOfAncestors(parent._id, 1, true);
|
|
await this.updateDescendantCountOfAncestors(parent._id, 1, true);
|
|
|
}
|
|
}
|
|
@@ -1973,7 +2002,7 @@ class PageService {
|
|
|
const newPath = Page.getRevertDeletedPageName(page.path);
|
|
const newPath = Page.getRevertDeletedPageName(page.path);
|
|
|
const originPage = await Page.findByPath(newPath);
|
|
const originPage = await Page.findByPath(newPath);
|
|
|
if (originPage != null) {
|
|
if (originPage != null) {
|
|
|
- throw Error(`This page cannot be reverted since a page with path "${originPage.path}" already exists.`);
|
|
|
|
|
|
|
+ throw new PathAlreadyExistsError('already_exists', originPage.path);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (isRecursively) {
|
|
if (isRecursively) {
|
|
@@ -1990,6 +2019,8 @@ class PageService {
|
|
|
}, { new: true });
|
|
}, { new: true });
|
|
|
await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
|
|
await PageTagRelation.updateMany({ relatedPage: page._id }, { $set: { isPageTrashed: false } });
|
|
|
|
|
|
|
|
|
|
+ this.pageEvent.emit('revert', page, user);
|
|
|
|
|
+
|
|
|
return updatedPage;
|
|
return updatedPage;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -2208,7 +2239,7 @@ class PageService {
|
|
|
// Create activity
|
|
// Create activity
|
|
|
const parameters = {
|
|
const parameters = {
|
|
|
user: user._id,
|
|
user: user._id,
|
|
|
- targetModel: ActivityDefine.MODEL_PAGE,
|
|
|
|
|
|
|
+ targetModel: SUPPORTED_TARGET_MODEL_TYPE.MODEL_PAGE,
|
|
|
target: page,
|
|
target: page,
|
|
|
action,
|
|
action,
|
|
|
};
|
|
};
|
|
@@ -2222,6 +2253,68 @@ class PageService {
|
|
|
await inAppNotificationService.emitSocketIo(targetUsers);
|
|
await inAppNotificationService.emitSocketIo(targetUsers);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ async normalizeParentByPath(path: string, user): Promise<void> {
|
|
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
|
|
+
|
|
|
|
|
+ const pages = await Page.findByPathAndViewer(path, user, null, false);
|
|
|
|
|
+ if (pages == null || !Array.isArray(pages)) {
|
|
|
|
|
+ throw Error('Something went wrong while converting pages.');
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pages.length === 0) {
|
|
|
|
|
+ throw new V5ConversionError(`Could not find the page "${path}" to convert.`, V5ConversionErrCode.PAGE_NOT_FOUND);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (pages.length > 1) {
|
|
|
|
|
+ throw new V5ConversionError(
|
|
|
|
|
+ `There are more than two pages at the path "${path}". Please rename or delete the page first.`,
|
|
|
|
|
+ V5ConversionErrCode.DUPLICATE_PAGES_FOUND,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const page = pages[0];
|
|
|
|
|
+ const {
|
|
|
|
|
+ grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
|
|
|
|
|
+ } = page;
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * UserGroup & Owner validation
|
|
|
|
|
+ */
|
|
|
|
|
+ let isGrantNormalized = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const shouldCheckDescendants = true;
|
|
|
|
|
+
|
|
|
|
|
+ isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ logger.error(`Failed to validate grant of page at "${path}"`, err);
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!isGrantNormalized) {
|
|
|
|
|
+ throw new V5ConversionError(
|
|
|
|
|
+ 'This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.',
|
|
|
|
|
+ V5ConversionErrCode.GRANT_INVALID,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let pageOp;
|
|
|
|
|
+ try {
|
|
|
|
|
+ pageOp = await PageOperation.create({
|
|
|
|
|
+ actionType: PageActionType.NormalizeParent,
|
|
|
|
|
+ actionStage: PageActionStage.Main,
|
|
|
|
|
+ page,
|
|
|
|
|
+ user,
|
|
|
|
|
+ fromPath: page.path,
|
|
|
|
|
+ toPath: page.path,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ catch (err) {
|
|
|
|
|
+ logger.error('Failed to create PageOperation document.', err);
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // no await
|
|
|
|
|
+ this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async normalizeParentByPageIds(pageIds: ObjectIdLike[], user, isRecursively: boolean): Promise<void> {
|
|
async normalizeParentByPageIds(pageIds: ObjectIdLike[], user, isRecursively: boolean): Promise<void> {
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
|
|
|
|
@@ -2440,7 +2533,11 @@ class PageService {
|
|
|
|
|
|
|
|
const { prevDescendantCount } = options;
|
|
const { prevDescendantCount } = options;
|
|
|
const newDescendantCount = pageAfterUpdatingDescendantCount.descendantCount;
|
|
const newDescendantCount = pageAfterUpdatingDescendantCount.descendantCount;
|
|
|
- const inc = (newDescendantCount - prevDescendantCount) + 1;
|
|
|
|
|
|
|
+ let inc = newDescendantCount - prevDescendantCount;
|
|
|
|
|
+ const isAlreadyConverted = page.parent != null;
|
|
|
|
|
+ if (!isAlreadyConverted) {
|
|
|
|
|
+ inc += 1;
|
|
|
|
|
+ }
|
|
|
await this.updateDescendantCountOfAncestors(page._id, inc, false);
|
|
await this.updateDescendantCountOfAncestors(page._id, inc, false);
|
|
|
}
|
|
}
|
|
|
catch (err) {
|
|
catch (err) {
|