Explorar o código

Added validation & Improved i18n error message

Taichi Masuyama %!s(int64=4) %!d(string=hai) anos
pai
achega
03cc1c8dde

+ 5 - 1
packages/app/resource/locales/en_US/translation.json

@@ -660,7 +660,11 @@
       "description": "Enter a path and all pages under that path will be converted to v5 compatible format.",
       "description": "Enter a path and all pages under that path will be converted to v5 compatible format.",
       "button_label": "Convert",
       "button_label": "Convert",
       "success": "Successfully requested conversion.",
       "success": "Successfully requested conversion.",
-      "error": "Failed to request conversion."
+      "error": "Failed to request conversion.",
+      "error_grant_invalid": "Page permissions are incorrect. Please correct it and try again.",
+      "error_page_restricted": "Pages in this path cannot be converted to v5 compatible format.",
+      "error_page_not_found": "Page not found.",
+      "error_duplicate_pages_found": "Multiple pages with the same path name were found. Please rename or delete and try again."
     }
     }
   },
   },
   "security_setting": {
   "security_setting": {

+ 5 - 1
packages/app/resource/locales/ja_JP/translation.json

@@ -659,7 +659,11 @@
       "description": "パスを入力することで、そのパスの配下のページを全て v5 互換形式に変換します",
       "description": "パスを入力することで、そのパスの配下のページを全て v5 互換形式に変換します",
       "button_label": "変換",
       "button_label": "変換",
       "success": "正常に変換を開始しました",
       "success": "正常に変換を開始しました",
-      "error": "変換を開始できませんでした"
+      "error": "変換を開始できませんでした",
+      "error_grant_invalid": "ページの権限が正しくありません。修正してから再度実行してください",
+      "error_page_restricted": "このパスのページは v5 互換形式に変換することが出来ません",
+      "error_page_not_found": "ページが見つかりませんでした",
+      "error_duplicate_pages_found": "同名のパスを持つページが複数見つかりました。リネームまたは削除してから再度実行してください"
     }
     }
   },
   },
   "security_setting": {
   "security_setting": {

+ 5 - 1
packages/app/resource/locales/zh_CN/translation.json

@@ -946,7 +946,11 @@
       "description": "输入一个路径,该路径下的所有页面将被转换为v5兼容格式。",
       "description": "输入一个路径,该路径下的所有页面将被转换为v5兼容格式。",
       "button_label": "转换",
       "button_label": "转换",
       "success": "成功地请求转换。",
       "success": "成功地请求转换。",
-      "error": "请求转换失败。"
+      "error": "请求转换失败。",
+      "error_grant_invalid": "页面权限不正确。请更正并重试。",
+      "error_page_restricted": "此路径中的页面不能被转换为v5兼容格式。",
+      "error_page_not_found": "没有找到页面。",
+      "error_duplicate_pages_found": "发现多个具有相同路径名称的页面。请重新命名或删除并重试。"
     }
     }
   },
   },
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",

+ 23 - 2
packages/app/src/components/PrivateLegacyPages.tsx

@@ -28,6 +28,7 @@ import SearchControl from './SearchPage/SearchControl';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { useSWRxV5MigrationStatus } from '~/stores/page-listing';
 import { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import { V5MigrationStatus } from '~/interfaces/page-listing-results';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 
 
 
 
 // TODO: replace with "customize:showPageLimitationS"
 // TODO: replace with "customize:showPageLimitationS"
@@ -398,8 +399,28 @@ const PrivateLegacyPages = (props: Props): JSX.Element => {
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             toastSuccess(t('private_legacy_pages.by_path_modal.success'));
             setOpenConvertModal(false);
             setOpenConvertModal(false);
           }
           }
-          catch {
-            toastError(t('private_legacy_pages.by_path_modal.error'));
+          catch (errs) {
+            if (errs.length === 1) {
+              switch (errs[0].code) {
+                case V5ConversionErrCode.GRANT_INVALID:
+                  toastError(t('private_legacy_pages.by_path_modal.error_grant_invalid'));
+                  break;
+                case V5ConversionErrCode.PAGE_RESTRICTED:
+                  toastError(t('private_legacy_pages.by_path_modal.error_page_restricted'));
+                  break;
+                case V5ConversionErrCode.PAGE_NOT_FOUND:
+                  toastError(t('private_legacy_pages.by_path_modal.error_page_not_found'));
+                  break;
+                case V5ConversionErrCode.DUPLICATE_PAGES_FOUND:
+                  toastError(t('private_legacy_pages.by_path_modal.error_duplicate_pages_found'));
+                  break;
+                default:
+                  toastError(t('private_legacy_pages.by_path_modal.error'));
+              }
+            }
+            else {
+              toastError(t('private_legacy_pages.by_path_modal.error'));
+            }
           }
           }
         }}
         }}
       />
       />

+ 8 - 0
packages/app/src/interfaces/errors/v5-conversion-error.ts

@@ -0,0 +1,8 @@
+export const V5ConversionErrCode = {
+  GRANT_INVALID: 'GrantInvalid',
+  PAGE_RESTRICTED: 'PageRestricted',
+  PAGE_NOT_FOUND: 'PageNotFound',
+  DUPLICATE_PAGES_FOUND: 'DuplicatePagesFound',
+} as const;
+
+export type V5ConversionErrCode = typeof V5ConversionErrCode[keyof typeof V5ConversionErrCode];

+ 0 - 0
packages/app/src/server/models/vo/error-search.ts → packages/app/src/server/models/vo/search-error.ts


+ 28 - 0
packages/app/src/server/models/vo/v5-conversion-error.ts

@@ -0,0 +1,28 @@
+import ExtensibleCustomError from 'extensible-custom-error';
+
+import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
+
+export class V5ConversionError extends ExtensibleCustomError {
+
+  readonly id = 'V5ConversionError'
+
+  code!: V5ConversionErrCode
+
+  constructor(message: string, code: V5ConversionErrCode) {
+    super(message);
+    this.code = code;
+  }
+
+}
+
+export const isV5ConversionError = (err: any): err is V5ConversionError => {
+  if (err == null || typeof err !== 'object') {
+    return false;
+  }
+
+  if (err instanceof V5ConversionError) {
+    return true;
+  }
+
+  return err?.id === 'V5ConversionError';
+};

+ 6 - 0
packages/app/src/server/routes/apiv3/pages.js

@@ -2,6 +2,7 @@ import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
+import { isV5ConversionError } from '../../models/vo/v5-conversion-error';
 
 
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:pages'); // eslint-disable-line no-unused-vars
 const { pathUtils, pagePathUtils } = require('@growi/core');
 const { pathUtils, pagePathUtils } = require('@growi/core');
@@ -795,6 +796,11 @@ module.exports = (crowi) => {
       }
       }
       catch (err) {
       catch (err) {
         logger.error(err);
         logger.error(err);
+
+        if (isV5ConversionError(err)) {
+          return res.apiv3Err(new ErrorV3(err.message, err.code), 400);
+        }
+
         return res.apiv3Err(new ErrorV3('Failed to convert pages.'), 400);
         return res.apiv3Err(new ErrorV3('Failed to convert pages.'), 400);
       }
       }
 
 

+ 1 - 1
packages/app/src/server/routes/search.ts

@@ -1,5 +1,5 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
-import { isSearchError } from '../models/vo/error-search';
+import { isSearchError } from '../models/vo/search-error';
 
 
 const logger = loggerFactory('growi:routes:search');
 const logger = loggerFactory('growi:routes:search');
 
 

+ 36 - 5
packages/app/src/server/service/page.ts

@@ -31,6 +31,8 @@ import { PageRedirectModel } from '../models/page-redirect';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
 import Subscription from '../models/subscription';
 import Subscription from '../models/subscription';
 import ActivityDefine from '../util/activityDefine';
 import ActivityDefine from '../util/activityDefine';
+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');
 
 
@@ -2254,13 +2256,42 @@ class PageService {
   async normalizeParentByPath(path: string, user): Promise<void> {
   async normalizeParentByPath(path: string, user): Promise<void> {
     const Page = mongoose.model('Page') as unknown as PageModel;
     const Page = mongoose.model('Page') as unknown as PageModel;
 
 
-    const page = await Page.findByPathAndViewer(path, user);
+    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);
+    }
 
 
-    if (page == null) {
-      throw Error(`Could not find the page "${path}" to convert.`);
+    const page = pages[0];
+    const {
+      grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
+    } = page;
+
+    /*
+     * UserGroup & Owner validation
+     */
+    if (grant !== Page.GRANT_RESTRICTED) {
+      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);
+      }
     }
     }
-    if (Array.isArray(page)) {
-      throw Error('page must not be an array');
+    else {
+      throw new V5ConversionError('Restricted pages can not be migrated', V5ConversionErrCode.PAGE_RESTRICTED);
     }
     }
 
 
     let pageOp;
     let pageOp;

+ 1 - 1
packages/app/src/server/service/search.ts

@@ -17,7 +17,7 @@ import { PageModel } from '../models/page';
 import { serializeUserSecurely } from '../models/serializers/user-serializer';
 import { serializeUserSecurely } from '../models/serializers/user-serializer';
 
 
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { ObjectIdLike } from '../interfaces/mongoose-utils';
-import { SearchError } from '../models/vo/error-search';
+import { SearchError } from '../models/vo/search-error';
 
 
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:service:search');
 const logger = loggerFactory('growi:service:search');