Просмотр исходного кода

change onlyDuplicateUserRelatedGrantedGroups to onlyDuplicateUserRelatedResources

Futa Arai 2 лет назад
Родитель
Сommit
0309e081c3

+ 4 - 2
apps/app/public/static/locales/en_US/translation.json

@@ -390,10 +390,12 @@
       "Current page name": "Current page name",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
-      "Same page already exists": "Same page already exists"
+      "Same page already exists": "Same page already exists",
+      "Only duplicate user related resources": "Only duplicate user related resources"
     },
     "help": {
-      "recursive": "Duplicate children of under this path recursively"
+      "recursive": "Duplicate children of under this path recursively",
+      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
     }
   },
   "duplicated_pages": "{{fromPath}} has been duplicated",

+ 4 - 2
apps/app/public/static/locales/ja_JP/translation.json

@@ -423,10 +423,12 @@
       "Current page name": "現在のページ名",
       "Recursively": "再帰的に複製",
       "Duplicate without exist path": "存在するパス以外を複製する",
-      "Same page already exists": "同じページがすでに存在します"
+      "Same page already exists": "同じページがすでに存在します",
+      "Only duplicate user related resources": "ユーザに関連のあるリソースのみを複製する"
     },
     "help": {
-      "recursive": "配下のページも複製します"
+      "recursive": "配下のページも複製します",
+      "only_user_related_resources": "ユーザが閲覧可能なページのみを複製します。また、閲覧権限が「特定グループのみ」で設定されている場合、複製後のページにはユーザが所属するグループのみを閲覧可能なグループとして設定します。"
     }
   },
   "duplicated_pages": "{{fromPath}} を複製しました",

+ 4 - 2
apps/app/public/static/locales/zh_CN/translation.json

@@ -380,10 +380,12 @@
 			"Current page name": "Current page name",
       "Recursively": "Recursively",
       "Duplicate without exist path": "Duplicate without exist path",
-      "Same page already exists": "Same page already exists"
+      "Same page already exists": "Same page already exists",
+      "Only duplicate user related resources": "Only duplicate user related resources"
     },
     "help": {
-      "recursive": "Duplicate children of under this path recursively"
+      "recursive": "Duplicate children of under this path recursively",
+      "only_user_related_resources": "This will only duplicate pages that the user has permission to view. If the page permission is set to \"Only specific groups\", only user related groups will be set to the page duplicate."
     }
   },
   "duplicated_pages": "{{fromPath}} 已重复",

+ 10 - 8
apps/app/src/components/PageDuplicateModal.tsx

@@ -37,7 +37,7 @@ const PageDuplicateModal = (): JSX.Element => {
   const [existingPaths, setExistingPaths] = useState<string[]>([]);
   const [isDuplicateRecursively, setIsDuplicateRecursively] = useState(true);
   const [isDuplicateRecursivelyWithoutExistPath, setIsDuplicateRecursivelyWithoutExistPath] = useState(true);
-  const [onlyDuplicateUserRelatedGrantedGroups, setOnlyDuplicateUserRelatedGrantedGroups] = useState(true);
+  const [onlyDuplicateUserRelatedResources, setOnlyDuplicateUserRelatedResources] = useState(false);
 
   const updateSubordinatedList = useCallback(async() => {
     if (page == null) {
@@ -116,7 +116,7 @@ const PageDuplicateModal = (): JSX.Element => {
     const { pageId, path } = page;
     try {
       const { data } = await apiv3Post('/pages/duplicate', {
-        pageId, pageNameInput, isRecursively: isDuplicateRecursively, onlyDuplicateUserRelatedGrantedGroups,
+        pageId, pageNameInput, isRecursively: isDuplicateRecursively, onlyDuplicateUserRelatedResources,
       });
       const onDuplicated = duplicateModalData?.opts?.onDuplicated;
       const fromPath = path;
@@ -130,7 +130,7 @@ const PageDuplicateModal = (): JSX.Element => {
     catch (err) {
       setErrs(err);
     }
-  }, [closeDuplicateModal, duplicateModalData?.opts?.onDuplicated, isDuplicateRecursively, page, pageNameInput]);
+  }, [closeDuplicateModal, duplicateModalData?.opts?.onDuplicated, isDuplicateRecursively, page, pageNameInput, onlyDuplicateUserRelatedResources]);
 
   useEffect(() => {
     if (isOpened) {
@@ -223,6 +223,7 @@ const PageDuplicateModal = (): JSX.Element => {
                 />
                 <label className="form-label form-check-label" htmlFor="cbDuplicatewithoutExistRecursively">
                   { t('modal_duplicate.label.Duplicate without exist path') }
+                  <p className="form-text text-muted my-0">{ t('modal_duplicate.help.recursive') }</p>
                 </label>
               </div>
             )}
@@ -232,13 +233,14 @@ const PageDuplicateModal = (): JSX.Element => {
         <div className="form-check form-check-warning mb-3">
           <input
             className="form-check-input"
-            id="cbOnlyDuplicateUserRelatedGrantedGroups"
+            id="cbOnlyDuplicateUserRelatedResources"
             type="checkbox"
-            checked={onlyDuplicateUserRelatedGrantedGroups}
-            onChange={() => setOnlyDuplicateUserRelatedGrantedGroups(!onlyDuplicateUserRelatedGrantedGroups)}
+            checked={onlyDuplicateUserRelatedResources}
+            onChange={() => setOnlyDuplicateUserRelatedResources(!onlyDuplicateUserRelatedResources)}
           />
-          <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedGrantedGroups">
-            Only duplicate user related granted groups
+          <label className="form-label form-check-label" htmlFor="cbOnlyDuplicateUserRelatedResources">
+            { t('modal_duplicate.label.Only duplicate user related resources') }
+            <p className="form-text text-muted my-0">{ t('modal_duplicate.help.only_user_related_resources') }</p>
           </label>
         </div>
         <div>

+ 2 - 2
apps/app/src/server/routes/apiv3/pages.js

@@ -797,7 +797,7 @@ module.exports = (crowi) => {
    */
   router.post('/duplicate', accessTokenParser, loginRequiredStrictly, excludeReadOnlyUser, addActivity, validator.duplicatePage, apiV3FormValidator,
     async(req, res) => {
-      const { pageId, isRecursively, onlyDuplicateUserRelatedGrantedGroups } = req.body;
+      const { pageId, isRecursively, onlyDuplicateUserRelatedResources } = req.body;
 
       const newPagePath = normalizePath(req.body.pageNameInput);
 
@@ -833,7 +833,7 @@ module.exports = (crowi) => {
         return res.apiv3Err(new ErrorV3(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 401);
       }
 
-      const newParentPage = await crowi.pageService.duplicate(page, newPagePath, req.user, isRecursively, onlyDuplicateUserRelatedGrantedGroups);
+      const newParentPage = await crowi.pageService.duplicate(page, newPagePath, req.user, isRecursively, onlyDuplicateUserRelatedResources);
       const result = { page: serializePageSecurely(newParentPage) };
 
       // copy the page since it's used and updated in crowi.pageService.duplicate

+ 9 - 1
apps/app/src/server/service/page-grant.ts

@@ -96,7 +96,8 @@ export interface IPageGrantService {
   validateGrantChange: (user, previousGrantedGroupIds: IGrantedGroup[], grant?: PageGrant, grantedGroupIds?: IGrantedGroup[]) => Promise<boolean>
   getUserRelatedGroups: (user) => Promise<PopulatedGrantedGroup[]>,
   filterGrantedGroupsByIds: (page: PageDocument, groupIds: string[]) => IGrantedGroup[],
-  getUserRelatedGrantedGroups: (page: PageDocument, user) => Promise<IGrantedGroup[]>
+  getUserRelatedGrantedGroups: (page: PageDocument, user) => Promise<IGrantedGroup[]>,
+  isUserGrantedPageAccess: (page: PageDocument, user, userRelatedGroupIds: string[]) => boolean
 }
 
 class PageGrantService implements IPageGrantService {
@@ -681,6 +682,13 @@ class PageGrantService implements IPageGrantService {
     return this.filterGrantedGroupsByIds(page, userRelatedGroupIds);
   }
 
+  isUserGrantedPageAccess(page: PageDocument, user, userRelatedGroupIds: string[]): boolean {
+    if (page.grant === PageGrant.GRANT_PUBLIC) return true;
+    if (page.grant === PageGrant.GRANT_OWNER) return page.grantedUsers?.includes(user._id.toString()) ?? false;
+    if (page.grant === PageGrant.GRANT_USER_GROUP) return this.filterGrantedGroupsByIds(page, userRelatedGroupIds).length > 0;
+    return false;
+  }
+
   /**
    * see: https://dev.growi.org/635a314eac6bcd85cbf359fc
    * @param {string} targetPath

+ 17 - 16
apps/app/src/server/service/page/index.ts

@@ -1056,7 +1056,7 @@ class PageService implements IPageService {
   /*
    * Duplicate
    */
-  async duplicate(page: PageDocument, newPagePath: string, user, isRecursively: boolean, onlyDuplicateUserRelatedGrantedGroups = false) {
+  async duplicate(page: PageDocument, newPagePath: string, user, isRecursively: boolean, onlyDuplicateUserRelatedResources = false) {
     /*
      * Common Operation
      */
@@ -1077,7 +1077,7 @@ class PageService implements IPageService {
     // 1. Separate v4 & v5 process
     const isShouldUseV4Process = shouldUseV4Process(page);
     if (isShouldUseV4Process) {
-      return this.duplicateV4(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedGrantedGroups);
+      return this.duplicateV4(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedResources);
     }
 
     const canOperate = await this.crowi.pageOperationService.canOperate(isRecursively, page.path, newPagePath);
@@ -1097,12 +1097,12 @@ class PageService implements IPageService {
       }
       grant = parent.grant;
       grantedUserIds = parent.grantedUsers;
-      grantedGroupIds = onlyDuplicateUserRelatedGrantedGroups ? (await this.pageGrantService.getUserRelatedGrantedGroups(parent, user)) : parent.grantedGroups;
+      grantedGroupIds = onlyDuplicateUserRelatedResources ? (await this.pageGrantService.getUserRelatedGrantedGroups(parent, user)) : parent.grantedGroups;
     }
     else {
       grant = page.grant;
       grantedUserIds = page.grantedUsers;
-      grantedGroupIds = onlyDuplicateUserRelatedGrantedGroups ? (await this.pageGrantService.getUserRelatedGrantedGroups(page, user)) : page.grantedGroups;
+      grantedGroupIds = onlyDuplicateUserRelatedResources ? (await this.pageGrantService.getUserRelatedGrantedGroups(page, user)) : page.grantedGroups;
     }
 
     if (grant !== Page.GRANT_RESTRICTED) {
@@ -1229,7 +1229,7 @@ class PageService implements IPageService {
     await PageOperation.findByIdAndDelete(pageOpId);
   }
 
-  async duplicateV4(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedGrantedGroups: boolean) {
+  async duplicateV4(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedResources: boolean) {
     const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
     // populate
     await page.populate({ path: 'revision', model: 'Revision', select: 'body' });
@@ -1248,7 +1248,7 @@ class PageService implements IPageService {
     this.pageEvent.emit('duplicate', page, user);
 
     if (isRecursively) {
-      this.duplicateDescendantsWithStream(page, newPagePath, user, onlyDuplicateUserRelatedGrantedGroups);
+      this.duplicateDescendantsWithStream(page, newPagePath, user, onlyDuplicateUserRelatedResources);
     }
 
     // take over tags
@@ -1304,7 +1304,7 @@ class PageService implements IPageService {
 
   private async duplicateDescendants(
       pages, user, oldPagePathPrefix, newPagePathPrefix,
-      onlyDuplicateUserRelatedGrantedGroups: boolean, shouldUseV4Process = true,
+      onlyDuplicateUserRelatedResources: boolean, shouldUseV4Process = true,
   ) {
     if (shouldUseV4Process) {
       return this.duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix);
@@ -1336,11 +1336,12 @@ class PageService implements IPageService {
       const revisionId = new mongoose.Types.ObjectId();
       pageIdMapping[page._id] = newPageId;
 
+      const isDuplicateTarget = !page.isEmpty
+      && (!onlyDuplicateUserRelatedResources || this.pageGrantService.isUserGrantedPageAccess(page, user, userRelatedGroupIds));
+
       let newPage;
-      if (!page.isEmpty) {
-        // TODO: GROUP_GRANT で、なおかつユーザが所属するグループが含まれている時のみ有効にすべき。そうでないと、誰もアクセスできないページとなってしまう。
-        // これはかなり複雑な仕様になるが、本当に他の方法はないのか検討
-        const grantedGroups = onlyDuplicateUserRelatedGrantedGroups
+      if (isDuplicateTarget) {
+        const grantedGroups = onlyDuplicateUserRelatedResources
           ? this.pageGrantService.filterGrantedGroupsByIds(page, userRelatedGroupIds)
           : page.grantedGroups;
         newPage = {
@@ -1411,9 +1412,9 @@ class PageService implements IPageService {
     await this.duplicateTags(pageIdMapping);
   }
 
-  private async duplicateDescendantsWithStream(page, newPagePath, user, onlyDuplicateUserRelatedGrantedGroups = false, shouldUseV4Process = true) {
+  private async duplicateDescendantsWithStream(page, newPagePath, user, onlyDuplicateUserRelatedResources = false, shouldUseV4Process = true) {
     if (shouldUseV4Process) {
-      return this.duplicateDescendantsWithStreamV4(page, newPagePath, user, onlyDuplicateUserRelatedGrantedGroups);
+      return this.duplicateDescendantsWithStreamV4(page, newPagePath, user, onlyDuplicateUserRelatedResources);
     }
 
     const iterableFactory = new PageCursorsForDescendantsFactory(user, page, true);
@@ -1432,7 +1433,7 @@ class PageService implements IPageService {
         try {
           count += batch.length;
           nNonEmptyDuplicatedPages += batch.filter(page => !page.isEmpty).length;
-          await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, onlyDuplicateUserRelatedGrantedGroups, shouldUseV4Process);
+          await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, onlyDuplicateUserRelatedResources, shouldUseV4Process);
           logger.debug(`Adding pages progressing: (count=${count})`);
         }
         catch (err) {
@@ -1459,7 +1460,7 @@ class PageService implements IPageService {
     return nNonEmptyDuplicatedPages;
   }
 
-  private async duplicateDescendantsWithStreamV4(page, newPagePath, user, onlyDuplicateUserRelatedGrantedGroups = false) {
+  private async duplicateDescendantsWithStreamV4(page, newPagePath, user, onlyDuplicateUserRelatedResources = false) {
     const readStream = await this.generateReadStreamToOperateOnlyDescendants(page.path, user);
 
     const newPagePathPrefix = newPagePath;
@@ -1473,7 +1474,7 @@ class PageService implements IPageService {
       async write(batch, encoding, callback) {
         try {
           count += batch.length;
-          await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, onlyDuplicateUserRelatedGrantedGroups);
+          await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, onlyDuplicateUserRelatedResources);
           logger.debug(`Adding pages progressing: (count=${count})`);
         }
         catch (err) {

+ 2 - 2
apps/app/test/integration/service/v5.non-public-page.test.ts

@@ -1093,10 +1093,10 @@ describe('PageService page operations with non-public pages', () => {
   });
   describe('Duplicate', () => {
 
-    const duplicate = async(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedGrantedGroups = false) => {
+    const duplicate = async(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedResources = false) => {
       // mock return value
       const mockedDuplicateRecursivelyMainOperation = jest.spyOn(crowi.pageService, 'duplicateRecursivelyMainOperation').mockReturnValue(null);
-      const duplicatedPage = await crowi.pageService.duplicate(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedGrantedGroups);
+      const duplicatedPage = await crowi.pageService.duplicate(page, newPagePath, user, isRecursively, onlyDuplicateUserRelatedResources);
 
       // retrieve the arguments passed when calling method duplicateRecursivelyMainOperation inside duplicate method
       const argsForDuplicateRecursivelyMainOperation = mockedDuplicateRecursivelyMainOperation.mock.calls[0];