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

Merge pull request #9082 from weseek/master

Release v7.0.19
Yuki Takei 1 год назад
Родитель
Сommit
357ad1a938

+ 3 - 11
.github/mergify.yml

@@ -3,19 +3,11 @@ queue_rules:
     allow_inplace_checks: false
     queue_conditions:
       - '#check-failure = 0'
-      - or:
-        - and:
-            - check-success ~= ci-slackbot-proxy-
-        - and:
-            - check-success ~= ci-app-
+      - check-success ~= ci-app-
     merge_conditions:
       - '#check-failure = 0'
-      - or:
-        - and:
-            - check-success ~= ci-slackbot-proxy-
-        - and:
-            - check-success ~= ci-app-
-            - check-success ~= test-prod-node20 /
+      - check-success ~= ci-app-
+      - check-success ~= test-prod-node20 /
 
 pull_request_rules:
   - name: Automatic queue to merge

+ 1 - 0
.github/workflows/ci-app-prod.yml

@@ -23,6 +23,7 @@ on:
       - master
       - dev/7.*.x
       - dev/6.*.x
+      - release/*
     types: [opened, reopened, synchronize]
     paths:
       - .github/mergify.yml

+ 2 - 2
.github/workflows/ci-app.yml

@@ -206,11 +206,11 @@ jobs:
           yarn global add node-gyp
           yarn --frozen-lockfile
 
-      - name: turbo run dev:ci
+      - name: turbo run launch-dev:ci
         working-directory: ./apps/app
         run: |
           cp config/ci/.env.local.for-ci .env.development.local
-          turbo run dev:ci --env-mode=loose
+          turbo run launch-dev:ci --env-mode=loose
         env:
           MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_dev
 

+ 2 - 2
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "7.0.18",
+  "version": "7.0.19-RC.0",
   "license": "MIT",
   "private": "true",
   "scripts": {
@@ -26,7 +26,7 @@
     "dev:migrate:up": "yarn dev:migrate-mongo up -f config/migrate-mongo-config.js",
     "dev:migrate:down": "yarn dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "//// for CI": "",
-    "dev:ci": "yarn cross-env NODE_ENV=development yarn ts-node src/server/app.ts --ci",
+    "launch-dev:ci": "yarn cross-env NODE_ENV=development yarn dev:migrate && yarn ts-node src/server/app.ts --ci",
     "lint:typecheck": "npx -y tspc",
     "lint:eslint": "yarn eslint --quiet \"**/*.{js,jsx,ts,tsx}\"",
     "lint:styles": "stylelint \"src/**/*.scss\"",

+ 6 - 0
apps/app/playwright/40-admin/access-to-admin-page.spec.ts

@@ -50,6 +50,12 @@ test('admin/export is successfully loaded', async({ page }) => {
   await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
 });
 
+test('admin/data-transfer is successfully loaded', async({ page }) => {
+  await page.goto('/admin/data-transfer');
+
+  await expect(page.getByTestId('admin-export-archive-data')).toBeVisible();
+});
+
 test('admin/notification is successfully loaded', async({ page }) => {
   await page.goto('/admin/notification');
 

+ 6 - 2
apps/app/src/client/components/PageComment.tsx

@@ -93,8 +93,12 @@ export const PageComment: FC<PageCommentProps> = memo((props: PageCommentProps):
       onDeleteCommentAfterOperation();
     }
     catch (error: unknown) {
-      setErrorMessageOnDelete(error as string);
-      toastError(`error: ${error}`);
+      const message = error instanceof Error
+        ? error.message
+        : (error as any).toString();
+
+      setErrorMessageOnDelete(message);
+      toastError(message);
     }
   }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
 

+ 1 - 0
apps/app/src/interfaces/apiv3/page.ts

@@ -42,4 +42,5 @@ export type IApiv3PageUpdateResponse = {
 
 export const PageUpdateErrorCode = {
   CONFLICT: 'conflict',
+  FORBIDDEN: 'forbidden',
 } as const;

+ 1 - 0
apps/app/src/server/models/page.ts

@@ -50,6 +50,7 @@ export interface PageDocument extends IPage, Document<Types.ObjectId> {
   [x:string]: any // for obsolete methods
   getLatestRevisionBodyLength(): Promise<number | null | undefined>
   calculateAndUpdateLatestRevisionBodyLength(this: PageDocument): Promise<void>
+  populateDataToShowRevision(shouldExcludeBody?: boolean): Promise<PageDocument>
 }
 
 

+ 15 - 5
apps/app/src/server/routes/apiv3/page/index.ts

@@ -20,6 +20,7 @@ import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user
 import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageDocument, PageModel } from '~/server/models/page';
 import { Revision } from '~/server/models/revision';
+import ShareLink from '~/server/models/share-link';
 import Subscription from '~/server/models/subscription';
 import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
@@ -202,6 +203,7 @@ module.exports = (crowi) => {
       query('pageId').optional().isString(),
       query('path').optional().isString(),
       query('findAll').optional().isBoolean(),
+      query('shareLinkId').optional().isMongoId(),
     ],
     likes: [
       body('pageId').isString(),
@@ -284,19 +286,27 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/Page'
    */
   router.get('/', certifySharedPage, accessTokenParser, loginRequired, validator.getPage, apiV3FormValidator, async(req, res) => {
-    const { user } = req;
+    const { user, isSharedPage } = req;
     const {
-      pageId, path, findAll, revisionId,
+      pageId, path, findAll, revisionId, shareLinkId,
     } = req.query;
 
-    if (pageId == null && path == null) {
-      return res.apiv3Err(new ErrorV3('Either parameter of path or pageId is required.', 'invalid-request'));
+    const isValid = (shareLinkId != null && pageId != null && path == null) || (shareLinkId == null && (pageId != null || path != null));
+    if (!isValid) {
+      return res.apiv3Err(new Error('Either parameter of (pageId or path) or (pageId and shareLinkId) is required.'), 400);
     }
 
     let page;
     let pages;
     try {
-      if (pageId != null) { // prioritized
+      if (isSharedPage) {
+        const shareLink = await ShareLink.findOne({ _id: shareLinkId });
+        if (shareLink == null) {
+          throw new Error('ShareLink is not found');
+        }
+        page = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+      }
+      else if (pageId != null) { // prioritized
         page = await Page.findByIdAndViewer(pageId, user);
       }
       else if (!findAll) {

+ 10 - 2
apps/app/src/server/routes/apiv3/page/update-page.ts

@@ -4,6 +4,7 @@ import type {
 } from '@growi/core';
 import { ErrorV3 } from '@growi/core/dist/models';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
+import { isTopPage, isUsersProtectedPages } from '@growi/core/dist/utils/page-path-utils';
 import type { Request, RequestHandler } from 'express';
 import type { ValidationChain } from 'express-validator';
 import { body } from 'express-validator';
@@ -27,6 +28,7 @@ import { apiV3FormValidator } from '../../../middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '../../../middlewares/exclude-read-only-user';
 import type { ApiV3Response } from '../interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page:update-page');
 
 
@@ -121,7 +123,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
     validator, apiV3FormValidator,
     async(req: UpdatePageRequest, res: ApiV3Response) => {
       const {
-        pageId, revisionId, body, origin,
+        pageId, revisionId, body, origin, grant,
       } = req.body;
 
       const sanitizeRevisionId = revisionId == null ? undefined : generalXssFilter.process(revisionId);
@@ -139,6 +141,12 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
         return res.apiv3Err(new ErrorV3(`Page('${pageId}' is not found or forbidden`, 'notfound_or_forbidden'), 400);
       }
 
+      const isGrantImmutable = isTopPage(currentPage.path) || isUsersProtectedPages(currentPage.path);
+
+      if (grant != null && grant !== currentPage.grant && isGrantImmutable) {
+        return res.apiv3Err(new ErrorV3('The grant settings for the specified page cannot be modified.', PageUpdateErrorCode.FORBIDDEN), 403);
+      }
+
       if (currentPage != null) {
         // Normalize the latest revision which was borken by the migration script '20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js'
         try {
@@ -164,7 +172,7 @@ export const updatePageHandlersFactory: UpdatePageHandlersFactory = (crowi) => {
       let previousRevision: IRevisionHasId | null;
       try {
         const {
-          grant, userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
+          userRelatedGrantUserGroupIds, overwriteScopesOfDescendants, wip,
         } = req.body;
         const options: IOptionsForUpdate = { overwriteScopesOfDescendants, origin, wip };
         if (grant != null) {

+ 4 - 3
apps/app/src/server/routes/comment.js

@@ -1,4 +1,5 @@
 
+import { getIdStringForRef } from '@growi/core';
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 import { Comment, CommentEvent, commentEvent } from '~/features/comment/server';
@@ -56,7 +57,6 @@ module.exports = function(crowi, app) {
   const logger = loggerFactory('growi:routes:comment');
   const User = crowi.model('User');
   const Page = crowi.model('Page');
-  const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const ApiResponse = require('../util/apiResponse');
 
   const activityEvent = crowi.event('activity');
@@ -465,6 +465,7 @@ module.exports = function(crowi, app) {
     }
 
     try {
+      /** @type {import('mongoose').HydratedDocument<import('~/interfaces/comment').IComment>} */
       const comment = await Comment.findById(commentId).exec();
 
       if (comment == null) {
@@ -472,12 +473,12 @@ module.exports = function(crowi, app) {
       }
 
       // check whether accessible
-      const pageId = comment.page;
+      const pageId = getIdStringForRef(comment.page);
       const isAccessible = await Page.isAccessiblePageByViewer(pageId, req.user);
       if (!isAccessible) {
         throw new Error('Current user is not accessible to this page.');
       }
-      if (req.user._id !== comment.creator.toString()) {
+      if (getIdStringForRef(req.user) !== getIdStringForRef(comment.creator)) {
         throw new Error('Current user is not operatable to this comment.');
       }
 

+ 11 - 4
apps/app/src/stores/page.tsx

@@ -57,10 +57,14 @@ export const useTemplateBodyData = (initialData?: string): SWRResponse<string, E
 export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|null): SWRResponse<IPagePopulatedToShowRevision|null> => {
   const key = 'currentPage';
 
+  const { data: isLatestRevision } = useIsLatestRevision();
+
   const { cache } = useSWRConfig();
 
   // Problem 1: https://github.com/weseek/growi/pull/7772/files#diff-4c1708c4f959974166c15435c6b35950ba01bbf35e7e4b8e99efeb125a8000a7
   // Problem 2: https://redmine.weseek.co.jp/issues/141027
+  // Problem 3: https://redmine.weseek.co.jp/issues/153618
+  // Problem 4: https://redmine.weseek.co.jp/issues/153759
   const shouldMutate = (() => {
     if (initialData === undefined) {
       return false;
@@ -81,8 +85,11 @@ export const useSWRxCurrentPage = (initialData?: IPagePopulatedToShowRevision|nu
       return true;
     }
 
-    // mutate When a different revision is opened
-    if (cachedData.revision?._id != null && initialData.revision?._id != null && cachedData.revision._id !== initialData.revision._id) {
+    // mutate when opening a previous revision.
+    if (!isLatestRevision
+        && cachedData.revision?._id != null && initialData.revision?._id != null
+        && cachedData.revision._id !== initialData.revision._id
+    ) {
       return true;
     }
 
@@ -285,7 +292,7 @@ export const useSWRxCurrentGrantData = (
     ? ['/page/grant-data', pageId]
     : null;
 
-  return useSWRImmutable(
+  return useSWR(
     key,
     ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );
@@ -295,7 +302,7 @@ export const useSWRxApplicableGrant = (
     pageId: string | null | undefined,
 ): SWRResponse<IRecordApplicableGrant, Error> => {
 
-  return useSWRImmutable(
+  return useSWR(
     pageId != null ? ['/page/applicable-grant', pageId] : null,
     ([endpoint, pageId]) => apiv3Get(endpoint, { pageId }).then(response => response.data),
   );

+ 3 - 2
apps/app/turbo.json

@@ -49,8 +49,9 @@
       "cache": false,
       "persistent": true
     },
-    "dev:ci": {
-      "dependsOn": ["^dev", "dev:migrate", "dev:styles-prebuilt"],
+
+    "launch-dev:ci": {
+      "dependsOn": ["^dev", "dev:styles-prebuilt"],
       "cache": false
     },
 

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.0.18-slackbot-proxy.0",
+  "version": "7.0.19-slackbot-proxy.0",
   "license": "MIT",
   "private": "true",
   "scripts": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.0.18",
+  "version": "7.0.19-RC.0",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "private": "true",