Jelajahi Sumber

Implemented

Taichi Masuyama 4 tahun lalu
induk
melakukan
263387737d

+ 10 - 6
packages/app/src/server/models/obsolete-page.js

@@ -239,6 +239,15 @@ export class PageQueryBuilder {
   }
 
   addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false) {
+    const condition = this.generateGrantCondition(user, userGroups, showAnyoneKnowsLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
+
+    this.query = this.query
+      .and(condition);
+
+    return this;
+  }
+
+  generateGrantCondition(user, userGroups, showAnyoneKnowsLink = false, showPagesRestrictedByOwner = false, showPagesRestrictedByGroup = false) {
     const grantConditions = [
       { grant: null },
       { grant: GRANT_PUBLIC },
@@ -272,12 +281,7 @@ export class PageQueryBuilder {
       );
     }
 
-    this.query = this.query
-      .and({
-        $or: grantConditions,
-      });
-
-    return this;
+    return { $or: grantConditions };
   }
 
   addConditionToPagenate(offset, limit, sortOpt) {

+ 22 - 7
packages/app/src/server/models/page.ts

@@ -44,7 +44,7 @@ export type CreateMethod = (path: string, body: string, user, options) => Promis
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete methods
   createEmptyPagesByPaths(paths: string[], onlyMigratedAsExistingPages?: boolean, publicOnly?: boolean): Promise<void>
-  getParentAndFillAncestors(path: string): Promise<PageDocument & { _id: any }>
+  getParentAndFillAncestors(path: string, user): Promise<PageDocument & { _id: any }>
   findByIdsAndViewer(pageIds: string[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
   findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean, includeEmpty?: boolean): Promise<PageDocument[]>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
@@ -118,15 +118,21 @@ const generateChildrenRegExp = (path: string): RegExp => {
   return new RegExp(`^${path}(\\/[^/]+)\\/?$`);
 };
 
-/*
+/**
  * Create empty pages if the page in paths didn't exist
+ * @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.
  */
-schema.statics.createEmptyPagesByPaths = async function(paths: string[], onlyMigratedAsExistingPages = true, publicOnly = false): Promise<void> {
+schema.statics.createEmptyPagesByPaths = async function(paths: string[], user: any | null, onlyMigratedAsExistingPages = true): Promise<void> {
   // find existing parents
-  const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }), true);
+  const builder = new PageQueryBuilder(this.find({}, { _id: 0, path: 1 }), true);
+
+  await this.addConditionToFilteringByViewerToEdit(builder, user);
+
   if (onlyMigratedAsExistingPages) {
     builder.addConditionAsMigrated();
   }
+
   const existingPages = await builder
     .addConditionToListByPathsArray(paths)
     .query
@@ -220,7 +226,7 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
  * @param path string
  * @returns Promise<PageDocument>
  */
-schema.statics.getParentAndFillAncestors = async function(path: string): Promise<PageDocument> {
+schema.statics.getParentAndFillAncestors = async function(path: string, user): Promise<PageDocument> {
   const parentPath = nodePath.dirname(path);
 
   const builder1 = new PageQueryBuilder(this.find({ path: parentPath }), true);
@@ -239,7 +245,7 @@ schema.statics.getParentAndFillAncestors = async function(path: string): Promise
   const ancestorPaths = collectAncestorPaths(path); // paths of parents need to be created
 
   // just create ancestors with empty pages
-  await this.createEmptyPagesByPaths(ancestorPaths);
+  await this.createEmptyPagesByPaths(ancestorPaths, user);
 
   // find ancestors
   const builder2 = new PageQueryBuilder(this.find(), true);
@@ -571,6 +577,15 @@ schema.statics.takeOffFromTree = async function(pageId: ObjectIdLike) {
   return this.findByIdAndUpdate(pageId, { $set: { parent: null } });
 };
 
+schema.statics.removeEmptyPagesByPaths = async function(paths: string[]): Promise<void> {
+  await this.deleteMany({
+    path: {
+      $in: paths,
+    },
+    isEmpty: true,
+  });
+};
+
 export type PageCreateOptions = {
   format?: string
   grantUserGroupId?: ObjectIdLike
@@ -661,7 +676,7 @@ export default (crowi: Crowi): any => {
     }
 
     let parentId: IObjectId | string | null = null;
-    const parent = await Page.getParentAndFillAncestors(path);
+    const parent = await Page.getParentAndFillAncestors(path, user);
     if (!isTopPage(path)) {
       parentId = parent._id;
     }

+ 58 - 49
packages/app/src/server/service/page.ts

@@ -460,7 +460,7 @@ class PageService {
       newParent = await this.getParentAndforceCreateEmptyTree(page, newPagePath);
     }
     else {
-      newParent = await Page.getParentAndFillAncestors(newPagePath);
+      newParent = await Page.getParentAndFillAncestors(newPagePath, user);
     }
 
     // 3. Put back target page to tree (also update the other attrs)
@@ -910,7 +910,7 @@ class PageService {
     };
     let duplicatedTarget;
     if (page.isEmpty) {
-      const parent = await Page.getParentAndFillAncestors(newPagePath);
+      const parent = await Page.getParentAndFillAncestors(newPagePath, user);
       duplicatedTarget = await Page.createEmptyPage(newPagePath, parent);
     }
     else {
@@ -963,7 +963,7 @@ class PageService {
     const shouldNormalize = this.shouldNormalizeParent(page);
     if (shouldNormalize) {
       try {
-        await this.normalizeParentAndDescendantCountOfDescendants(newPagePath);
+        await this.normalizeParentAndDescendantCountOfDescendants(newPagePath, user);
         logger.info(`Successfully normalized duplicated descendant pages under "${newPagePath}"`);
       }
       catch (err) {
@@ -1839,7 +1839,7 @@ class PageService {
     }
 
     // 2. Revert target
-    const parent = await Page.getParentAndFillAncestors(newPath);
+    const parent = await Page.getParentAndFillAncestors(newPath, user);
     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,
@@ -1886,7 +1886,7 @@ class PageService {
     const shouldNormalize = this.shouldNormalizeParent(page);
     if (shouldNormalize) {
       try {
-        await this.normalizeParentAndDescendantCountOfDescendants(newPath);
+        await this.normalizeParentAndDescendantCountOfDescendants(newPath, user);
         logger.info(`Successfully normalized reverted descendant pages under "${newPath}"`);
       }
       catch (err) {
@@ -2253,7 +2253,7 @@ class PageService {
     }
     else {
       // getParentAndFillAncestors
-      const parent = await Page.getParentAndFillAncestors(page.path);
+      const parent = await Page.getParentAndFillAncestors(page.path, user);
       updatedPage = await Page.findOneAndUpdate({ _id: page._id }, { parent: parent._id }, { new: true });
     }
 
@@ -2326,7 +2326,7 @@ class PageService {
     // TODO: insertOne PageOperationBlock
 
     try {
-      await this.normalizeParentRecursively([page.path]);
+      await this.normalizeParentRecursively([page.path], user);
     }
     catch (err) {
       logger.error('V5 initial miration failed.', err);
@@ -2425,7 +2425,7 @@ class PageService {
 
     // then migrate
     try {
-      await this.normalizeParentRecursively(['/'], true);
+      await this.normalizeParentRecursively(['/'], null);
     }
     catch (err) {
       logger.error('V5 initial miration failed.', err);
@@ -2460,40 +2460,48 @@ class PageService {
     }
   }
 
-  private async normalizeParentAndDescendantCountOfDescendants(path: string): Promise<void> {
-    await this.normalizeParentRecursively([path]);
+  private async normalizeParentAndDescendantCountOfDescendants(path: string, user): Promise<void> {
+    await this.normalizeParentRecursively([path], user);
 
     // update descendantCount of descendant pages
     await this.updateDescendantCountOfSelfAndDescendants(path);
   }
 
-  async normalizeParentRecursively(paths: string[], publicOnly = false): Promise<void> {
+  /**
+   * Normalize parent attribute by passing paths and user.
+   * @param paths Pages under this paths value will be updated.
+   * @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> {
+    const Page = mongoose.model('Page') as unknown as PageModel;
+    const { PageQueryBuilder } = Page;
+
     const ancestorPaths = paths.flatMap(p => collectAncestorPaths(p, [p]));
     const regexps = paths.map(p => new RegExp(`^${escapeStringRegexp(addTrailingSlash(p))}`, 'i'));
 
-    return this._normalizeParentRecursively(regexps, ancestorPaths, publicOnly);
+    // determine UserGroup condition
+    let userGroups = null;
+    if (user != null) {
+      const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
+      userGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
+    }
+
+    const grantFiltersByUser: { $or: any[] } = PageQueryBuilder.generateGrantCondition(user, userGroups);
+
+    return this._normalizeParentRecursively(regexps, ancestorPaths, grantFiltersByUser, user);
   }
 
   // TODO: use websocket to show progress
-  private async _normalizeParentRecursively(regexps: RegExp[], pathsToInclude: string[], publicOnly: boolean): Promise<void> {
+  private async _normalizeParentRecursively(regexps: RegExp[], pathsToInclude: string[], grantFiltersByUser: { $or: any[] }, user): Promise<void> {
     const BATCH_SIZE = 100;
     const PAGES_LIMIT = 1000;
+    const publicOnly = grantFiltersByUser == null;
+
     const Page = mongoose.model('Page') as unknown as PageModel;
     const { PageQueryBuilder } = Page;
 
-    // GRANT_RESTRICTED and GRANT_SPECIFIED will never have parent
-    const grantFilter: any = {
-      $and: [
-        { grant: { $ne: Page.GRANT_RESTRICTED } },
-        { grant: { $ne: Page.GRANT_SPECIFIED } },
-      ],
-    };
-
-    if (publicOnly) { // add grant condition if not null
-      grantFilter.$and = [...grantFilter.$and, { grant: Page.GRANT_PUBLIC }];
-    }
-
-    // generate filter
+    // Build filter
     const filter: any = {
       $and: [
         {
@@ -2518,11 +2526,9 @@ class PageService {
       });
     }
 
-    const total = await Page.countDocuments(filter);
-
     let baseAggregation = Page
       .aggregate([
-        { $match: grantFilter },
+        { $match: grantFiltersByUser },
         { $match: filter },
         {
           $project: { // minimize data to fetch
@@ -2533,6 +2539,7 @@ class PageService {
       ]);
 
     // limit pages to get
+    const total = await Page.countDocuments(filter);
     if (total > PAGES_LIMIT) {
       baseAggregation = baseAggregation.limit(Math.floor(total * 0.3));
     }
@@ -2545,18 +2552,15 @@ class PageService {
     let countPages = 0;
     let shouldContinue = true;
 
-    // migrate all siblings for each page
     const migratePagesStream = new Writable({
       objectMode: true,
       async write(pages, encoding, callback) {
-        // make list to create empty pages
-        const parentPathsSet = new Set<string>(pages.map(page => pathlib.dirname(page.path)));
-        const parentPaths = Array.from(parentPathsSet);
+        const parentPaths = Array.from(new Set<string>(pages.map(p => pathlib.dirname(p.path))));
 
-        // fill parents with empty pages
-        await Page.createEmptyPagesByPaths(parentPaths, false, publicOnly);
+        // Fill parents with empty pages
+        await Page.createEmptyPagesByPaths(parentPaths, user, false);
 
-        // find parents again
+        // Find parents
         const builder = new PageQueryBuilder(Page.find({}, { _id: 1, path: 1 }), true);
         const parents = await builder
           .addConditionToListByPathsArray(parentPaths)
@@ -2564,21 +2568,22 @@ class PageService {
           .lean()
           .exec();
 
-        // bulkWrite to update parent
+        // Normalize all siblings for each page
         const updateManyOperations = parents.map((parent) => {
           const parentId = parent._id;
 
-          // modify to adjust for RegExp
-          let parentPath = parent.path === '/' ? '' : parent.path;
-          parentPath = escapeStringRegexp(parentPath);
-
+          // Build filter
+          const parentPathEscaped = escapeStringRegexp(parent.path === '/' ? '' : parent.path); // adjust the path for RegExp
           const filter: any = {
-            // regexr.com/6889f
-            // ex. /parent/any_child OR /any_level1
-            path: { $regex: new RegExp(`^${parentPath}(\\/[^/]+)\\/?$`, 'i') },
+            $and: [{
+              path: { $regex: new RegExp(`^${parentPathEscaped}(\\/[^/]+)\\/?$`, 'i') }, // see: regexr.com/6889f (e.g. /parent/any_child or /any_level1)
+            }],
           };
           if (publicOnly) {
-            filter.grant = Page.GRANT_PUBLIC;
+            filter.$and.push({ grant: Page.GRANT_PUBLIC });
+          }
+          else {
+            filter.$and.push(grantFiltersByUser);
           }
 
           return {
@@ -2592,16 +2597,17 @@ class PageService {
         });
         try {
           const res = await Page.bulkWrite(updateManyOperations);
+
           countPages += res.result.nModified;
           logger.info(`Page migration processing: (count=${countPages})`);
 
-          // throw
+          // Throw if any error is found
           if (res.result.writeErrors.length > 0) {
             logger.error('Failed to migrate some pages', res.result.writeErrors);
             throw Error('Failed to migrate some pages');
           }
 
-          // finish migration
+          // Finish migration if no modification occurred
           if (res.result.nModified === 0 && res.result.nMatched === 0) {
             shouldContinue = false;
             logger.error('Migration is unable to continue', 'parentPaths:', parentPaths, 'bulkWriteResult:', res);
@@ -2612,6 +2618,9 @@ class PageService {
           throw err;
         }
 
+        // Remove unnecessary empty pages
+        await Page.removeEmptyPagesByPaths(pages.map(p => p.path));
+
         callback();
       },
       final(callback) {
@@ -2625,9 +2634,9 @@ class PageService {
 
     await streamToPromise(migratePagesStream);
 
-    const existsFilter = { $and: [...grantFilter.$and, ...filter.$and] };
+    const existsFilter = { $and: [grantFiltersByUser, ...filter.$and] };
     if (await Page.exists(existsFilter) && shouldContinue) {
-      return this._normalizeParentRecursively(regexps, pathsToInclude, publicOnly);
+      return this._normalizeParentRecursively(regexps, pathsToInclude, grantFiltersByUser, user);
     }
 
   }