|
|
@@ -20,7 +20,7 @@ import { IUserHasId } from '~/interfaces/user';
|
|
|
import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
|
|
|
import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
|
|
|
import {
|
|
|
- CreateMethod, PageCreateOptions, PageModel, PageDocument,
|
|
|
+ CreateMethod, PageCreateOptions, PageModel, PageDocument, pushRevision,
|
|
|
} from '~/server/models/page';
|
|
|
import { createBatchStream } from '~/server/util/batch-stream';
|
|
|
import loggerFactory from '~/utils/logger';
|
|
|
@@ -525,7 +525,7 @@ class PageService {
|
|
|
newParent = await this.getParentAndforceCreateEmptyTree(page, newPagePath);
|
|
|
}
|
|
|
else {
|
|
|
- newParent = await Page.getParentAndFillAncestors(newPagePath, user);
|
|
|
+ newParent = await this.getParentAndFillAncestorsByUser(user, newPagePath);
|
|
|
}
|
|
|
|
|
|
// 3. Put back target page to tree (also update the other attrs)
|
|
|
@@ -979,12 +979,12 @@ class PageService {
|
|
|
};
|
|
|
let duplicatedTarget;
|
|
|
if (page.isEmpty) {
|
|
|
- const parent = await Page.getParentAndFillAncestors(newPagePath, user);
|
|
|
+ const parent = await this.getParentAndFillAncestorsByUser(user, newPagePath);
|
|
|
duplicatedTarget = await Page.createEmptyPage(newPagePath, parent);
|
|
|
}
|
|
|
else {
|
|
|
await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
|
|
|
- duplicatedTarget = await (Page.create as CreateMethod)(
|
|
|
+ duplicatedTarget = await (this.create as CreateMethod)(
|
|
|
newPagePath, page.revision.body, user, options,
|
|
|
);
|
|
|
}
|
|
|
@@ -1067,7 +1067,6 @@ class PageService {
|
|
|
}
|
|
|
|
|
|
async duplicateV4(page, newPagePath, user, isRecursively) {
|
|
|
- const Page = this.crowi.model('Page');
|
|
|
const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
|
|
|
// populate
|
|
|
await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
|
|
|
@@ -1080,7 +1079,7 @@ class PageService {
|
|
|
|
|
|
newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
|
|
|
|
|
|
- const createdPage = await Page.create(
|
|
|
+ const createdPage = await this.crowi.pageService.create(
|
|
|
newPagePath, page.revision.body, user, options,
|
|
|
);
|
|
|
this.pageEvent.emit('duplicate', page, user);
|
|
|
@@ -1910,7 +1909,7 @@ class PageService {
|
|
|
}
|
|
|
|
|
|
// 2. Revert target
|
|
|
- const parent = await Page.getParentAndFillAncestors(newPath, user);
|
|
|
+ const parent = await this.getParentAndFillAncestorsByUser(user, newPath);
|
|
|
const updatedPage = await Page.findByIdAndUpdate(page._id, {
|
|
|
$set: {
|
|
|
path: newPath, status: Page.STATUS_PUBLISHED, lastUpdateUser: user._id, deleteUser: null, deletedAt: null, parent: parent._id, descendantCount: 0,
|
|
|
@@ -2255,13 +2254,28 @@ class PageService {
|
|
|
|
|
|
async normalizeParentByPath(path: string, user): Promise<void> {
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+ const { PageQueryBuilder } = Page;
|
|
|
+
|
|
|
+ // This validation is not 100% correct since it ignores user to count
|
|
|
+ const builder = new PageQueryBuilder(Page.find());
|
|
|
+ builder.addConditionAsNotMigrated();
|
|
|
+ builder.addConditionToListWithDescendants(path);
|
|
|
+ const nEstimatedNormalizationTarget: number = await builder.query.exec('count');
|
|
|
+ if (nEstimatedNormalizationTarget === 0) {
|
|
|
+ throw Error('No page is available for conversion');
|
|
|
+ }
|
|
|
|
|
|
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);
|
|
|
+ const isForbidden = await Page.count({ path, isEmpty: false }) > 0;
|
|
|
+ if (isForbidden) {
|
|
|
+ throw new V5ConversionError('It is not allowed to convert this page.', V5ConversionErrCode.FORBIDDEN);
|
|
|
+ }
|
|
|
}
|
|
|
if (pages.length > 1) {
|
|
|
throw new V5ConversionError(
|
|
|
@@ -2270,10 +2284,33 @@ class PageService {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- const page = pages[0];
|
|
|
- const {
|
|
|
- grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
|
|
|
- } = page;
|
|
|
+ let page;
|
|
|
+ let systematicallyCreatedPage;
|
|
|
+
|
|
|
+ const shouldCreateNewPage = pages[0] == null;
|
|
|
+ if (shouldCreateNewPage) {
|
|
|
+ const notEmptyParent = await Page.findNotEmptyParentByPathRecursively(path);
|
|
|
+
|
|
|
+ const options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] | undefined } = {
|
|
|
+ grant: notEmptyParent.grant,
|
|
|
+ grantUserGroupId: notEmptyParent.grantedGroup,
|
|
|
+ grantedUsers: notEmptyParent.grantedUsers,
|
|
|
+ };
|
|
|
+
|
|
|
+ systematicallyCreatedPage = await this.createBySystem(
|
|
|
+ path,
|
|
|
+ '',
|
|
|
+ options,
|
|
|
+ );
|
|
|
+ page = systematicallyCreatedPage;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ page = pages[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ const grant = page.grant;
|
|
|
+ const grantedUserIds = page.grantedUsers;
|
|
|
+ const grantedGroupId = page.grantedGroup;
|
|
|
|
|
|
/*
|
|
|
* UserGroup & Owner validation
|
|
|
@@ -2311,7 +2348,6 @@ class PageService {
|
|
|
throw err;
|
|
|
}
|
|
|
|
|
|
- // no await
|
|
|
this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
|
|
|
}
|
|
|
|
|
|
@@ -2412,7 +2448,7 @@ class PageService {
|
|
|
normalizedPage = await Page.findById(page._id);
|
|
|
}
|
|
|
else {
|
|
|
- const parent = await Page.getParentAndFillAncestors(page.path, user);
|
|
|
+ const parent = await this.getParentAndFillAncestorsByUser(user, page.path);
|
|
|
normalizedPage = await Page.findOneAndUpdate({ _id: page._id }, { parent: parent._id }, { new: true });
|
|
|
}
|
|
|
|
|
|
@@ -2466,7 +2502,7 @@ class PageService {
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
const { PageQueryBuilder } = Page;
|
|
|
const builder = new PageQueryBuilder(Page.findOne());
|
|
|
- builder.addConditionAsMigrated();
|
|
|
+ builder.addConditionAsOnTree();
|
|
|
builder.addConditionToListByPathsArray([page.path]);
|
|
|
const existingPage = await builder.query.exec();
|
|
|
|
|
|
@@ -2509,18 +2545,19 @@ class PageService {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
|
|
|
+ async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<number> {
|
|
|
// Save prevDescendantCount for sub-operation
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
const { PageQueryBuilder } = Page;
|
|
|
const builder = new PageQueryBuilder(Page.findOne(), true);
|
|
|
- builder.addConditionAsMigrated();
|
|
|
+ builder.addConditionAsOnTree();
|
|
|
builder.addConditionToListByPathsArray([page.path]);
|
|
|
const exPage = await builder.query.exec();
|
|
|
const options = { prevDescendantCount: exPage?.descendantCount ?? 0 };
|
|
|
|
|
|
+ let count: number;
|
|
|
try {
|
|
|
- await this.normalizeParentRecursively([page.path], user);
|
|
|
+ count = await this.normalizeParentRecursively([page.path], user);
|
|
|
}
|
|
|
catch (err) {
|
|
|
logger.error('V5 initial miration failed.', err);
|
|
|
@@ -2536,6 +2573,8 @@ class PageService {
|
|
|
}
|
|
|
|
|
|
await this.normalizeParentRecursivelySubOperation(page, user, pageOp._id, options);
|
|
|
+
|
|
|
+ return count;
|
|
|
}
|
|
|
|
|
|
async normalizeParentRecursivelySubOperation(page, user, pageOpId: ObjectIdLike, options: {prevDescendantCount: number}): Promise<void> {
|
|
|
@@ -2674,7 +2713,7 @@ class PageService {
|
|
|
* @param user To be used to filter pages to update. If null, only public pages will be updated.
|
|
|
* @returns Promise<void>
|
|
|
*/
|
|
|
- async normalizeParentRecursively(paths: string[], user: any | null): Promise<void> {
|
|
|
+ async normalizeParentRecursively(paths: string[], user: any | null): Promise<number> {
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
|
|
|
const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, []));
|
|
|
@@ -2740,7 +2779,7 @@ class PageService {
|
|
|
|
|
|
private async _normalizeParentRecursively(
|
|
|
pathOrRegExps: (RegExp | string)[], publicPathsToNormalize: string[], grantFiltersByUser: { $or: any[] }, user, count = 0, skiped = 0, isFirst = true,
|
|
|
- ): Promise<void> {
|
|
|
+ ): Promise<number> {
|
|
|
const BATCH_SIZE = 100;
|
|
|
const PAGES_LIMIT = 1000;
|
|
|
|
|
|
@@ -2779,6 +2818,9 @@ class PageService {
|
|
|
let nextCount = count;
|
|
|
let nextSkiped = skiped;
|
|
|
|
|
|
+ // eslint-disable-next-line max-len
|
|
|
+ const buildPipelineToCreateEmptyPagesByUser = this.buildPipelineToCreateEmptyPagesByUser.bind(this);
|
|
|
+
|
|
|
const migratePagesStream = new Writable({
|
|
|
objectMode: true,
|
|
|
async write(pages, encoding, callback) {
|
|
|
@@ -2817,7 +2859,8 @@ class PageService {
|
|
|
{ path: { $nin: publicPathsToNormalize }, status: Page.STATUS_PUBLISHED },
|
|
|
];
|
|
|
const filterForApplicableAncestors = { $or: orFilters };
|
|
|
- await Page.createEmptyPagesByPaths(parentPaths, user, false, filterForApplicableAncestors);
|
|
|
+ const aggregationPipeline = await buildPipelineToCreateEmptyPagesByUser(user, parentPaths, false, filterForApplicableAncestors);
|
|
|
+ await Page.createEmptyPagesByPaths(parentPaths, aggregationPipeline);
|
|
|
|
|
|
// 3. Find parents
|
|
|
const addGrantCondition = (builder) => {
|
|
|
@@ -2910,6 +2953,8 @@ class PageService {
|
|
|
|
|
|
// End
|
|
|
socket.emit(SocketEventName.PMEnded, { isSucceeded: true });
|
|
|
+
|
|
|
+ return nextCount;
|
|
|
}
|
|
|
|
|
|
private async _v5NormalizeIndex() {
|
|
|
@@ -2963,7 +3008,7 @@ class PageService {
|
|
|
const { PageQueryBuilder } = Page;
|
|
|
|
|
|
const builder = new PageQueryBuilder(Page.find(), true);
|
|
|
- builder.addConditionAsMigrated();
|
|
|
+ builder.addConditionAsOnTree();
|
|
|
builder.addConditionToListWithDescendants(path);
|
|
|
builder.addConditionToSortPagesByDescPath();
|
|
|
|
|
|
@@ -3008,6 +3053,407 @@ class PageService {
|
|
|
socket.emit(SocketEventName.UpdateDescCount, data);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Build the base aggregation pipeline for fillAncestors--- methods
|
|
|
+ * @param onlyMigratedAsExistingPages Determine whether to include non-migrated pages as existing pages. If a page exists,
|
|
|
+ * an empty page will not be created at that page's path.
|
|
|
+ */
|
|
|
+ private buildBasePipelineToCreateEmptyPages(paths: string[], onlyMigratedAsExistingPages = true, andFilter?): any[] {
|
|
|
+ const aggregationPipeline: any[] = [];
|
|
|
+
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // -- Filter by paths
|
|
|
+ aggregationPipeline.push({ $match: { path: { $in: paths } } });
|
|
|
+ // -- Normalized condition
|
|
|
+ if (onlyMigratedAsExistingPages) {
|
|
|
+ aggregationPipeline.push({
|
|
|
+ $match: {
|
|
|
+ $or: [
|
|
|
+ { grant: Page.GRANT_PUBLIC },
|
|
|
+ { parent: { $ne: null } },
|
|
|
+ { path: '/' },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ });
|
|
|
+ }
|
|
|
+ // -- Add custom pipeline
|
|
|
+ if (andFilter != null) {
|
|
|
+ aggregationPipeline.push({ $match: andFilter });
|
|
|
+ }
|
|
|
+
|
|
|
+ return aggregationPipeline;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async buildPipelineToCreateEmptyPagesByUser(user, paths: string[], onlyMigratedAsExistingPages = true, andFilter?): Promise<any[]> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ const pipeline = this.buildBasePipelineToCreateEmptyPages(paths, onlyMigratedAsExistingPages, andFilter);
|
|
|
+ let userGroups = null;
|
|
|
+ if (user != null) {
|
|
|
+ const UserGroupRelation = mongoose.model('UserGroupRelation') as any;
|
|
|
+ userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
|
|
|
+ }
|
|
|
+ const grantCondition = Page.generateGrantCondition(user, userGroups);
|
|
|
+ pipeline.push({ $match: grantCondition });
|
|
|
+
|
|
|
+ return pipeline;
|
|
|
+ }
|
|
|
+
|
|
|
+ private buildPipelineToCreateEmptyPagesBySystem(paths: string[]): any[] {
|
|
|
+ return this.buildBasePipelineToCreateEmptyPages(paths);
|
|
|
+ }
|
|
|
+
|
|
|
+ private async connectPageTree(path: string): Promise<void> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+ const { PageQueryBuilder } = Page;
|
|
|
+
|
|
|
+ const ancestorPaths = collectAncestorPaths(path);
|
|
|
+
|
|
|
+ // Find ancestors
|
|
|
+ const builder = new PageQueryBuilder(Page.find(), true);
|
|
|
+ builder.addConditionToFilterByApplicableAncestors(ancestorPaths); // avoid including not normalized pages
|
|
|
+ const ancestors = await builder
|
|
|
+ .addConditionToListByPathsArray(ancestorPaths)
|
|
|
+ .addConditionToSortPagesByDescPath()
|
|
|
+ .query
|
|
|
+ .exec();
|
|
|
+
|
|
|
+ // Update parent attrs
|
|
|
+ const ancestorsMap = new Map(); // Map<path, page>
|
|
|
+ ancestors.forEach(page => !ancestorsMap.has(page.path) && ancestorsMap.set(page.path, page)); // the earlier element should be the true ancestor
|
|
|
+
|
|
|
+ const nonRootAncestors = ancestors.filter(page => !isTopPage(page.path));
|
|
|
+ const operations = nonRootAncestors.map((page) => {
|
|
|
+ const parentPath = pathlib.dirname(page.path);
|
|
|
+ return {
|
|
|
+ updateOne: {
|
|
|
+ filter: {
|
|
|
+ _id: page._id,
|
|
|
+ },
|
|
|
+ update: {
|
|
|
+ parent: ancestorsMap.get(parentPath)._id,
|
|
|
+ },
|
|
|
+ },
|
|
|
+ };
|
|
|
+ });
|
|
|
+ await Page.bulkWrite(operations);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Find parent or create parent if not exists.
|
|
|
+ * It also updates parent of ancestors
|
|
|
+ * @param path string
|
|
|
+ * @returns Promise<PageDocument>
|
|
|
+ */
|
|
|
+ async getParentAndFillAncestorsByUser(user, path: string): Promise<PageDocument> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // Find parent
|
|
|
+ const parent = await Page.findParentByPath(path);
|
|
|
+ if (parent != null) {
|
|
|
+ return parent;
|
|
|
+ }
|
|
|
+
|
|
|
+ const ancestorPaths = collectAncestorPaths(path);
|
|
|
+
|
|
|
+ // Fill ancestors
|
|
|
+ const aggregationPipeline: any[] = await this.buildPipelineToCreateEmptyPagesByUser(user, ancestorPaths);
|
|
|
+
|
|
|
+ await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
|
|
|
+
|
|
|
+ // Connect ancestors
|
|
|
+ await this.connectPageTree(path);
|
|
|
+
|
|
|
+ // Return the created parent
|
|
|
+ const createdParent = await Page.findParentByPath(path);
|
|
|
+ if (createdParent == null) {
|
|
|
+ throw Error('Failed to find the created parent by getParentAndFillAncestorsByUser');
|
|
|
+ }
|
|
|
+ return createdParent;
|
|
|
+ }
|
|
|
+
|
|
|
+ async getParentAndFillAncestorsBySystem(path: string): Promise<PageDocument> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // Find parent
|
|
|
+ const parent = await Page.findParentByPath(path);
|
|
|
+ if (parent != null) {
|
|
|
+ return parent;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Fill ancestors
|
|
|
+ const ancestorPaths = collectAncestorPaths(path);
|
|
|
+ const aggregationPipeline: any[] = this.buildPipelineToCreateEmptyPagesBySystem(ancestorPaths);
|
|
|
+
|
|
|
+ await Page.createEmptyPagesByPaths(ancestorPaths, aggregationPipeline);
|
|
|
+
|
|
|
+ // Connect ancestors
|
|
|
+ await this.connectPageTree(path);
|
|
|
+
|
|
|
+ // Return the created parent
|
|
|
+ const createdParent = await Page.findParentByPath(path);
|
|
|
+ if (createdParent == null) {
|
|
|
+ throw Error('Failed to find the created parent by getParentAndFillAncestorsByUser');
|
|
|
+ }
|
|
|
+
|
|
|
+ return createdParent;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --------- Create ---------
|
|
|
+
|
|
|
+ private async preparePageDocumentToCreate(path: string, shouldNew: boolean): Promise<PageDocument> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ const emptyPage = await Page.findOne({ path, isEmpty: true });
|
|
|
+
|
|
|
+ // Use empty page if exists, if not, create a new page
|
|
|
+ let page;
|
|
|
+ if (shouldNew) {
|
|
|
+ page = new Page();
|
|
|
+ }
|
|
|
+ else if (emptyPage != null) {
|
|
|
+ page = emptyPage;
|
|
|
+ const descendantCount = await Page.recountDescendantCount(page._id);
|
|
|
+
|
|
|
+ page.descendantCount = descendantCount;
|
|
|
+ page.isEmpty = false;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ page = new Page();
|
|
|
+ }
|
|
|
+
|
|
|
+ return page;
|
|
|
+ }
|
|
|
+
|
|
|
+ private setFieldExceptForGrantRevisionParent(
|
|
|
+ pageDocument: PageDocument,
|
|
|
+ path: string,
|
|
|
+ user?,
|
|
|
+ ): void {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ pageDocument.path = path;
|
|
|
+ pageDocument.creator = user;
|
|
|
+ pageDocument.lastUpdateUser = user;
|
|
|
+ pageDocument.status = Page.STATUS_PUBLISHED;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async canProcessCreate(
|
|
|
+ path: string,
|
|
|
+ grantData: {
|
|
|
+ grant: number,
|
|
|
+ grantedUserIds?: ObjectIdLike[],
|
|
|
+ grantUserGroupId?: ObjectIdLike,
|
|
|
+ },
|
|
|
+ shouldValidateGrant: boolean,
|
|
|
+ user?,
|
|
|
+ ): Promise<boolean> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // Operatability validation
|
|
|
+ const canOperate = await this.crowi.pageOperationService.canOperate(false, null, path);
|
|
|
+ if (!canOperate) {
|
|
|
+ logger.error(`Cannot operate create to path "${path}" right now.`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Existance validation
|
|
|
+ const isExist = (await Page.count({ path, isEmpty: false })) > 0; // not validate empty page
|
|
|
+ if (isExist) {
|
|
|
+ logger.error('Cannot create new page to existed path');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // UserGroup & Owner validation
|
|
|
+ const { grant, grantedUserIds, grantUserGroupId } = grantData;
|
|
|
+ if (shouldValidateGrant) {
|
|
|
+ if (user == null) {
|
|
|
+ throw Error('user is required to validate grant');
|
|
|
+ }
|
|
|
+
|
|
|
+ let isGrantNormalized = false;
|
|
|
+ try {
|
|
|
+ // It must check descendants as well if emptyTarget is not null
|
|
|
+ const isEmptyPageAlreadyExist = await Page.count({ path, isEmpty: true }) > 0;
|
|
|
+ const shouldCheckDescendants = isEmptyPageAlreadyExist;
|
|
|
+
|
|
|
+ isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantUserGroupId, shouldCheckDescendants);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ logger.error(`Failed to validate grant of page at "${path}" of grant ${grant}:`, err);
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
+ if (!isGrantNormalized) {
|
|
|
+ throw Error('The selected grant or grantedGroup is not assignable to this page.');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ async create(path: string, body: string, user, options: PageCreateOptions = {}): Promise<PageDocument> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // Switch method
|
|
|
+ const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
|
|
|
+ if (!isV5Compatible) {
|
|
|
+ return Page.createV4(path, body, user, options);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Values
|
|
|
+ // eslint-disable-next-line no-param-reassign
|
|
|
+ path = this.crowi.xss.process(path); // sanitize path
|
|
|
+ const {
|
|
|
+ format = 'markdown', grantUserGroupId,
|
|
|
+ } = options;
|
|
|
+ const grant = isTopPage(path) ? Page.GRANT_PUBLIC : options.grant;
|
|
|
+ const grantData = {
|
|
|
+ grant,
|
|
|
+ grantedUserIds: grant === Page.GRANT_OWNER ? [user._id] : undefined,
|
|
|
+ grantUserGroupId,
|
|
|
+ };
|
|
|
+
|
|
|
+ const isGrantRestricted = grant === Page.GRANT_RESTRICTED;
|
|
|
+
|
|
|
+ // Validate
|
|
|
+ const shouldValidateGrant = !isGrantRestricted;
|
|
|
+ const canProcessCreate = await this.canProcessCreate(path, grantData, shouldValidateGrant, user);
|
|
|
+ if (!canProcessCreate) {
|
|
|
+ throw Error('Cannnot process create');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prepare a page document
|
|
|
+ const shouldNew = isGrantRestricted;
|
|
|
+ const page = await this.preparePageDocumentToCreate(path, shouldNew);
|
|
|
+
|
|
|
+ // Set field
|
|
|
+ this.setFieldExceptForGrantRevisionParent(page, path, user);
|
|
|
+
|
|
|
+ // Apply scope
|
|
|
+ page.applyScope(user, grant, grantUserGroupId);
|
|
|
+
|
|
|
+ // Set parent
|
|
|
+ if (isTopPage(path) || isGrantRestricted) { // set parent to null when GRANT_RESTRICTED
|
|
|
+ page.parent = null;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ const parent = await this.getParentAndFillAncestorsByUser(user, path);
|
|
|
+ page.parent = parent._id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Save
|
|
|
+ let savedPage = await page.save();
|
|
|
+
|
|
|
+ // Create revision
|
|
|
+ const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
|
|
|
+ const newRevision = Revision.prepareRevision(savedPage, body, null, user, { format });
|
|
|
+ savedPage = await pushRevision(savedPage, newRevision, user);
|
|
|
+ await savedPage.populateDataToShowRevision();
|
|
|
+
|
|
|
+ // Update descendantCount
|
|
|
+ await this.updateDescendantCountOfAncestors(savedPage._id, 1, false);
|
|
|
+
|
|
|
+ // Emit create event
|
|
|
+ this.pageEvent.emit('create', savedPage, user);
|
|
|
+
|
|
|
+ // Delete PageRedirect if exists
|
|
|
+ const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;
|
|
|
+ try {
|
|
|
+ await PageRedirect.deleteOne({ fromPath: path });
|
|
|
+ logger.warn(`Deleted page redirect after creating a new page at path "${path}".`);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ // no throw
|
|
|
+ logger.error('Failed to delete PageRedirect');
|
|
|
+ }
|
|
|
+
|
|
|
+ return savedPage;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async canProcessCreateBySystem(
|
|
|
+ path: string,
|
|
|
+ grantData: {
|
|
|
+ grant: number,
|
|
|
+ grantedUserIds?: ObjectIdLike[],
|
|
|
+ grantUserGroupId?: ObjectIdLike,
|
|
|
+ },
|
|
|
+ ): Promise<boolean> {
|
|
|
+ return this.canProcessCreate(path, grantData, false);
|
|
|
+ }
|
|
|
+
|
|
|
+ async createBySystem(path: string, body: string, options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] }): Promise<PageDocument> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
|
|
|
+ if (!isV5Compatible) {
|
|
|
+ throw Error('This method is available only when v5 compatible');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Values
|
|
|
+ // eslint-disable-next-line no-param-reassign
|
|
|
+ path = this.crowi.xss.process(path); // sanitize path
|
|
|
+
|
|
|
+ const {
|
|
|
+ format = 'markdown', grantUserGroupId, grantedUsers,
|
|
|
+ } = options;
|
|
|
+ const grant = isTopPage(path) ? Page.GRANT_PUBLIC : options.grant;
|
|
|
+
|
|
|
+ const isGrantRestricted = grant === Page.GRANT_RESTRICTED;
|
|
|
+ const isGrantOwner = grant === Page.GRANT_OWNER;
|
|
|
+
|
|
|
+ const grantData = {
|
|
|
+ grant,
|
|
|
+ grantedUserIds: isGrantOwner ? grantedUsers : undefined,
|
|
|
+ grantUserGroupId,
|
|
|
+ };
|
|
|
+
|
|
|
+ // Validate
|
|
|
+ if (isGrantOwner && grantedUsers?.length !== 1) {
|
|
|
+ throw Error('grantedUser must exist when grant is GRANT_OWNER');
|
|
|
+ }
|
|
|
+ const canProcessCreateBySystem = await this.canProcessCreateBySystem(path, grantData);
|
|
|
+ if (!canProcessCreateBySystem) {
|
|
|
+ throw Error('Cannnot process createBySystem');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prepare a page document
|
|
|
+ const shouldNew = !isGrantRestricted;
|
|
|
+ const page = await this.preparePageDocumentToCreate(path, shouldNew);
|
|
|
+
|
|
|
+ // Set field
|
|
|
+ this.setFieldExceptForGrantRevisionParent(page, path);
|
|
|
+
|
|
|
+ // Apply scope
|
|
|
+ page.applyScope({ _id: grantedUsers?.[0] }, grant, grantUserGroupId);
|
|
|
+
|
|
|
+ // Set parent
|
|
|
+ if (isTopPage(path) || isGrantRestricted) { // set parent to null when GRANT_RESTRICTED
|
|
|
+ page.parent = null;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ const parent = await this.getParentAndFillAncestorsBySystem(path);
|
|
|
+ page.parent = parent._id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Save
|
|
|
+ let savedPage = await page.save();
|
|
|
+
|
|
|
+ // Create revision
|
|
|
+ const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
|
|
|
+ const dummyUser = { _id: new mongoose.Types.ObjectId() };
|
|
|
+ const newRevision = Revision.prepareRevision(savedPage, body, null, dummyUser, { format });
|
|
|
+ savedPage = await pushRevision(savedPage, newRevision, dummyUser);
|
|
|
+
|
|
|
+ // Update descendantCount
|
|
|
+ await this.updateDescendantCountOfAncestors(savedPage._id, 1, false);
|
|
|
+
|
|
|
+ // Emit create event
|
|
|
+ this.pageEvent.emit('create', savedPage, dummyUser);
|
|
|
+
|
|
|
+ return savedPage;
|
|
|
+ }
|
|
|
+
|
|
|
}
|
|
|
|
|
|
export default PageService;
|