Explorar o código

Merge branch 'master' into feat/ldap-group-sync

Futa Arai %!s(int64=2) %!d(string=hai) anos
pai
achega
a504513c2d
Modificáronse 48 ficheiros con 667 adicións e 148 borrados
  1. 18 1
      CHANGELOG.md
  2. 1 1
      apps/app/docker/README.md
  3. 1 1
      apps/app/package.json
  4. 1 0
      apps/app/src/components/Common/Dropdown/PageItemControl.tsx
  5. 1 1
      apps/app/src/components/Layout/MainPane.tsx
  6. 1 1
      apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx
  7. 1 1
      apps/app/src/components/Navbar/GrowiNavbarBottom.tsx
  8. 1 1
      apps/app/src/components/Page/CopyDropdown.jsx
  9. 1 1
      apps/app/src/components/Page/RenderTagLabels.tsx
  10. 3 1
      apps/app/src/components/Page/TagLabels.tsx
  11. 12 6
      apps/app/src/pages/share/[[...path]].page.tsx
  12. 2 0
      apps/app/src/server/crowi/index.js
  13. 0 54
      apps/app/src/server/middlewares/certify-shared-file.js
  14. 145 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts
  15. 49 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts
  16. 1 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/index.ts
  17. 4 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts
  18. 28 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts
  19. 25 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts
  20. 1 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/index.ts
  21. 59 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts
  22. 22 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts
  23. 131 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts
  24. 69 0
      apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts
  25. 1 1
      apps/app/src/server/middlewares/certify-shared-page.js
  26. 0 1
      apps/app/src/server/models/index.js
  27. 0 39
      apps/app/src/server/models/share-link.js
  28. 46 0
      apps/app/src/server/models/share-link.ts
  29. 1 2
      apps/app/src/server/routes/apiv3/security-settings/index.js
  30. 2 5
      apps/app/src/server/routes/apiv3/share-links.js
  31. 2 2
      apps/app/src/server/routes/index.js
  32. 1 1
      apps/app/src/server/service/page.ts
  33. 2 2
      apps/app/src/server/util/mongoose-utils.ts
  34. 1 1
      apps/slackbot-proxy/package.json
  35. 1 1
      package.json
  36. 1 1
      packages/core/package.json
  37. 1 1
      packages/hackmd/package.json
  38. 1 1
      packages/presentation/package.json
  39. 1 1
      packages/preset-templates/package.json
  40. 1 1
      packages/preset-themes/package.json
  41. 1 1
      packages/remark-attachment-refs/package.json
  42. 1 1
      packages/remark-drawio/package.json
  43. 1 1
      packages/remark-growi-directive/package.json
  44. 1 1
      packages/remark-lsx/package.json
  45. 11 2
      packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx
  46. 1 1
      packages/slack/package.json
  47. 1 1
      packages/ui/package.json
  48. 11 11
      yarn.lock

+ 18 - 1
CHANGELOG.md

@@ -1,9 +1,26 @@
 # Changelog
 
-## [Unreleased](https://github.com/weseek/growi/compare/v6.2.1...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v6.2.2...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v6.2.2](https://github.com/weseek/growi/compare/v6.2.1...v6.2.2) - 2023-10-30
+
+### 🚀 Improvement
+
+- imprv: Printing styles (#8195) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Show liker counts in lsx (#8194) @yuki-takei
+
+### 🧰 Maintenance
+
+- ci(deps-dev): bump postcss from 8.4.26 to 8.4.31 (#8142) @dependabot
+- ci(deps): bump cypress-io/github-action from 5 to 6 (#8051) @dependabot
+- ci(deps): bump amannn/action-semantic-pull-request from 5.0.2 to 5.3.0 (#8127) @dependabot
+- ci(deps): bump aws-actions/configure-aws-credentials from 2 to 4 (#8128) @dependabot
+
 ## [v6.2.1](https://github.com/weseek/growi/compare/v6.2.0...v6.2.1) - 2023-10-03
 
 ### BREAKING CHANGES

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`6.2.1`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.1/apps/app/docker/Dockerfile)
+* [`6.2.2`, `6.2`, `6`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v6.2.2/apps/app/docker/Dockerfile)
 * [`6.1.15`, `6.1` (Dockerfile)](https://github.com/weseek/growi/blob/v6.1.15/apps/app/docker/Dockerfile)
 * [`6.0.15`, `6.0` (Dockerfile)](https://github.com/weseek/growi/blob/v6.0.15/packages/app/docker/Dockerfile)
 * [`5.1.7`, `5.1`, `5` (Dockerfile)](https://github.com/weseek/growi/blob/v5.1.7/packages/app/docker/Dockerfile)

+ 1 - 1
apps/app/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/app",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "license": "MIT",
   "scripts": {
     "//// for production": "",

+ 1 - 0
apps/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -251,6 +251,7 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
 
   return (
     <DropdownMenu
+      className="d-print-none"
       data-testid="page-item-control-menu"
       right={alignRight}
       modifiers={getCustomModifiers(alignRight)}

+ 1 - 1
apps/app/src/components/Layout/MainPane.tsx

@@ -23,7 +23,7 @@ export const MainPane = (props: Props): JSX.Element => {
                   <div className="flex-grow-1 flex-basis-0 mw-0">
                     {children}
                   </div>
-                  <div className="grw-side-contents-container d-edit-none" data-vrt-blackout-side-contents>
+                  <div className="grw-side-contents-container d-edit-none d-print-none" data-vrt-blackout-side-contents>
                     <div className="grw-side-contents-sticky-container">
                       {sideContents}
                     </div>

+ 1 - 1
apps/app/src/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -371,7 +371,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
     return (
       <>
         <div className="d-flex">
-          <div className="d-flex flex-column align-items-end justify-content-center py-md-2" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
+          <div className="d-flex flex-column align-items-end justify-content-center py-md-2 d-print-none" style={{ gap: `${isCompactMode ? '5px' : '7px'}` }}>
             {isViewMode && (
               <div className="h-50">
                 {pageId != null && (

+ 1 - 1
apps/app/src/components/Navbar/GrowiNavbarBottom.tsx

@@ -24,7 +24,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   }
 
   return (
-    <div className="d-md-none d-edit-none fixed-bottom">
+    <div className="d-md-none d-edit-none d-print-none fixed-bottom">
 
       { isDeviceSmallerThanMd && !isSearchPage && (
         <div id="grw-global-search-collapse" className="grw-global-search collapse bg-dark">

+ 1 - 1
apps/app/src/components/Page/CopyDropdown.jsx

@@ -110,7 +110,7 @@ const CopyDropdown = (props) => {
 
   return (
     <>
-      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown`} isOpen={dropdownOpen} toggle={toggleDropdown}>
+      <Dropdown className={`${styles['grw-copy-dropdown']} grw-copy-dropdown d-print-none`} isOpen={dropdownOpen} toggle={toggleDropdown}>
         <DropdownToggle
           caret
           className={dropdownToggleClassName}

+ 1 - 1
apps/app/src/components/Page/RenderTagLabels.tsx

@@ -44,7 +44,7 @@ const RenderTagLabels = React.memo((props: RenderTagLabelsProps) => {
       })}
       <NotAvailableForGuest>
         <NotAvailableForReadOnlyUser>
-          <div id="edit-tags-btn-wrapper-for-tooltip">
+          <div id="edit-tags-btn-wrapper-for-tooltip" className="d-print-none">
             <a
               className={`btn btn-link btn-edit-tags text-muted p-0 d-flex align-items-center ${isTagsEmpty && 'no-tags'} ${isTagLabelsDisabled && 'disabled'}`}
               onClick={openEditorHandler}

+ 3 - 1
apps/app/src/components/Page/TagLabels.tsx

@@ -34,9 +34,11 @@ export const TagLabels:FC<Props> = (props: Props) => {
     return <TagLabelsSkeleton />;
   }
 
+  const printNoneClass = tags.length === 0 ? 'd-print-none' : '';
+
   return (
     <>
-      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center`} data-testid="grw-tag-labels">
+      <div className={`${styles['grw-tag-labels']} grw-tag-labels d-flex align-items-center ${printNoneClass}`} data-testid="grw-tag-labels">
         <i className="tag-icon icon-tag mr-2" />
         <RenderTagLabels
           tags={tags}

+ 12 - 6
apps/app/src/pages/share/[[...path]].page.tsx

@@ -1,6 +1,6 @@
 import React, { useEffect } from 'react';
 
-import type { IUserHasId, IPagePopulatedToShowRevision } from '@growi/core';
+import { type IUserHasId, type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
 import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
@@ -18,6 +18,7 @@ import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument } from '~/server/models/page';
+import ShareLink from '~/server/models/share-link';
 import {
   useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid, useIsEnabledMarp,
@@ -233,18 +234,23 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   const props: Props = result.props as Props;
 
   try {
-    const ShareLinkModel = crowi.model('ShareLink');
-    const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
+    const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate('relatedPage');
     if (shareLink == null) {
       props.isNotFound = true;
     }
     else {
       props.isNotFound = false;
-      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
-      props.skipSSR = await skipSSR(shareLink.relatedPage, ssrMaxRevisionBodyLength);
-      props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
+
+      // retrieve Page
+      const Page = crowi.model('Page');
+      const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+      // determine whether skip SSR
+      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('crowi', 'app:ssrMaxRevisionBodyLength');
+      props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
+      // populate
+      props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
     }
   }
   catch (err) {

+ 2 - 0
apps/app/src/server/crowi/index.js

@@ -22,6 +22,7 @@ import { projectRoot } from '~/utils/project-dir-utils';
 import UserEvent from '../events/user';
 import Activity from '../models/activity';
 import PageRedirect from '../models/page-redirect';
+import ShareLink from '../models/share-link';
 import Tag from '../models/tag';
 import UserGroup from '../models/user-group';
 import UserGroupRelation from '../models/user-group-relation';
@@ -313,6 +314,7 @@ Crowi.prototype.setupModels = async function() {
   crowiIndependent.UserGroup = UserGroup;
   crowiIndependent.UserGroupRelation = UserGroupRelation;
   crowiIndependent.PageRedirect = PageRedirect;
+  crowiIndependent.ShareLink = ShareLink;
 
   Object.keys(crowiIndependent).forEach((key) => {
     return this.model(key, crowiIndependent[key]);

+ 0 - 54
apps/app/src/server/middlewares/certify-shared-file.js

@@ -1,54 +0,0 @@
-import loggerFactory from '~/utils/logger';
-
-const url = require('url');
-
-const logger = loggerFactory('growi:middleware:certify-shared-fire');
-
-module.exports = (crowi) => {
-
-  return async(req, res, next) => {
-    const { referer } = req.headers;
-
-    // Attachments cannot be viewed by clients who do not send referer.
-    // https://github.com/weseek/growi/issues/2819
-    if (referer == null) {
-      return next();
-    }
-
-    const { path } = url.parse(referer);
-
-    if (!path.startsWith('/share/')) {
-      return next();
-    }
-
-    const fileId = req.params.id || null;
-
-    const Attachment = crowi.model('Attachment');
-    const ShareLink = crowi.model('ShareLink');
-
-    const attachment = await Attachment.findOne({ _id: fileId });
-
-    if (attachment == null) {
-      return next();
-    }
-
-    const shareLinks = await ShareLink.find({ relatedPage: attachment.page });
-
-    // If sharelinks don't exist, skip it
-    if (shareLinks.length === 0) {
-      return next();
-    }
-
-    // Is there a valid share link
-    shareLinks.map((sharelink) => {
-      if (!sharelink.isExpired()) {
-        logger.debug('Confirmed target file belong to a share page');
-        req.isSharedPage = true;
-      }
-      return;
-    });
-
-    next();
-  };
-
-};

+ 145 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.spec.ts

@@ -0,0 +1,145 @@
+import type { Response } from 'express';
+import { mock } from 'vitest-mock-extended';
+
+import { ShareLinkDocument } from '~/server/models/share-link';
+
+import { certifySharedPageAttachmentMiddleware, type RequestToAllowShareLink } from './certify-shared-page-attachment';
+import { ValidReferer } from './interfaces';
+
+const mocks = vi.hoisted(() => {
+  return {
+    validateRefererMock: vi.fn(),
+    retrieveValidShareLinkByRefererMock: vi.fn(),
+    validateAttachmentMock: vi.fn(),
+  };
+});
+
+vi.mock('./validate-referer', () => ({ validateReferer: mocks.validateRefererMock }));
+vi.mock('./retrieve-valid-share-link', () => ({ retrieveValidShareLinkByReferer: mocks.retrieveValidShareLinkByRefererMock }));
+vi.mock('./validate-attachment', () => ({ validateAttachment: mocks.validateAttachmentMock }));
+
+
+describe('certifySharedPageAttachmentMiddleware', () => {
+
+  const res = mock<Response>();
+  const next = vi.fn();
+
+  describe('should called next() without req.isSharedPage set', () => {
+
+    it('when the fileId param is null', async() => {
+      // setup
+      const req = mock<RequestToAllowShareLink>();
+      req.params = {}; // id: undefined
+      req.headers = {};
+
+      // when
+      await certifySharedPageAttachmentMiddleware(req, res, next);
+
+      // then
+      expect(mocks.validateRefererMock).not.toHaveBeenCalled();
+      expect(req.isSharedPage === true).toBeFalsy();
+      expect(next).toHaveBeenCalledOnce();
+    });
+
+    it('when validateReferer returns null', async() => {
+      // setup
+      const req = mock<RequestToAllowShareLink>();
+      req.params = { id: 'file id string' };
+      req.headers = { referer: 'referer string' };
+
+      // when
+      await certifySharedPageAttachmentMiddleware(req, res, next);
+
+      // then
+      expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
+      expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
+      expect(req.isSharedPage === true).toBeFalsy();
+      expect(next).toHaveBeenCalledOnce();
+    });
+
+    it('when retrieveValidShareLinkByReferer returns null', async() => {
+      // setup
+      const req = mock<RequestToAllowShareLink>();
+      req.params = { id: 'file id string' };
+      req.headers = { referer: 'referer string' };
+
+      const validReferer: ValidReferer = {
+        referer: 'referer string',
+        shareLinkId: 'ffffffffffffffffffffffff',
+      };
+      mocks.validateRefererMock.mockImplementation(() => validReferer);
+
+      mocks.retrieveValidShareLinkByRefererMock.mockResolvedValue(null);
+
+      // when
+      await certifySharedPageAttachmentMiddleware(req, res, next);
+
+      // then
+      expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
+      expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+      expect(req.isSharedPage === true).toBeFalsy();
+      expect(next).toHaveBeenCalledOnce();
+    });
+
+    it('when validateAttachment returns false', async() => {
+      // setup
+      const req = mock<RequestToAllowShareLink>();
+      req.params = { id: 'file id string' };
+      req.headers = { referer: 'referer string' };
+
+      const validReferer = vi.fn();
+      mocks.validateRefererMock.mockImplementation(() => validReferer);
+
+      const shareLinkMock = mock<ShareLinkDocument>();
+      mocks.retrieveValidShareLinkByRefererMock.mockResolvedValue(shareLinkMock);
+
+      mocks.validateAttachmentMock.mockResolvedValue(false);
+
+      // when
+      await certifySharedPageAttachmentMiddleware(req, res, next);
+
+      // then
+      expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
+      expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
+      expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+      expect(mocks.validateAttachmentMock).toHaveBeenCalledOnce();
+      expect(mocks.validateAttachmentMock).toHaveBeenCalledWith('file id string', shareLinkMock);
+      expect(req.isSharedPage === true).toBeFalsy();
+      expect(next).toHaveBeenCalledOnce();
+    });
+
+  });
+
+  it('should set req.isSharedPage true', async() => {
+    // setup
+    const req = mock<RequestToAllowShareLink>();
+    req.params = { id: 'file id string' };
+    req.headers = { referer: 'referer string' };
+
+    const validReferer = vi.fn();
+    mocks.validateRefererMock.mockImplementation(() => validReferer);
+
+    const shareLinkMock = mock<ShareLinkDocument>();
+    mocks.retrieveValidShareLinkByRefererMock.mockResolvedValue(shareLinkMock);
+
+    mocks.validateAttachmentMock.mockResolvedValue(true);
+
+    // when
+    await certifySharedPageAttachmentMiddleware(req, res, next);
+
+    // then
+    expect(mocks.validateRefererMock).toHaveBeenCalledOnce();
+    expect(mocks.validateRefererMock).toHaveBeenCalledWith('referer string');
+    expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledOnce();
+    expect(mocks.retrieveValidShareLinkByRefererMock).toHaveBeenCalledWith(validReferer);
+    expect(mocks.validateAttachmentMock).toHaveBeenCalledOnce();
+    expect(mocks.validateAttachmentMock).toHaveBeenCalledWith('file id string', shareLinkMock);
+
+    expect(req.isSharedPage === true).toBeTruthy();
+
+    expect(next).toHaveBeenCalledOnce();
+  });
+});

+ 49 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/certify-shared-page-attachment.ts

@@ -0,0 +1,49 @@
+import type { NextFunction, Request, Response } from 'express';
+
+import loggerFactory from '~/utils/logger';
+
+import { retrieveValidShareLinkByReferer } from './retrieve-valid-share-link';
+import { validateAttachment } from './validate-attachment';
+import { validateReferer } from './validate-referer';
+
+
+const logger = loggerFactory('growi:middleware:certify-shared-page-attachment');
+
+
+export interface RequestToAllowShareLink extends Request {
+  isSharedPage?: boolean,
+}
+
+export const certifySharedPageAttachmentMiddleware = async(req: RequestToAllowShareLink, res: Response, next: NextFunction): Promise<void> => {
+
+  const fileId: string | undefined = req.params.id;
+  const { referer } = req.headers;
+
+  if (fileId == null) {
+    logger.error('The param fileId is required. Please confirm to usage of this middleware.');
+    return next();
+  }
+
+  const validReferer = validateReferer(referer);
+  if (!validReferer) {
+    logger.info('invalid referer.');
+    return next();
+  }
+
+  logger.info('referer is valid.');
+
+  const shareLink = await retrieveValidShareLinkByReferer(validReferer);
+  if (shareLink == null) {
+    logger.info(`No valid ShareLink document found by the referer (${validReferer.referer}})`);
+    return next();
+  }
+
+  if (!(await validateAttachment(fileId, shareLink))) {
+    logger.info(`No valid ShareLink document found by the fileId (${fileId}) and referer (${validReferer.referer}})`);
+    return next();
+  }
+
+  req.isSharedPage = true;
+  next();
+
+};

+ 1 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/index.ts

@@ -0,0 +1 @@
+export * from './certify-shared-page-attachment';

+ 4 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/interfaces.ts

@@ -0,0 +1,4 @@
+export type ValidReferer = {
+  referer: string,
+  shareLinkId: string,
+};

+ 28 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/retrieve-valid-share-link.ts

@@ -0,0 +1,28 @@
+import type { ShareLinkDocument, ShareLinkModel } from '~/server/models/share-link';
+import { getModelSafely } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+import type { ValidReferer } from './interfaces';
+
+
+const logger = loggerFactory('growi:middleware:certify-shared-page-attachment:retrieve-valid-share-link');
+
+
+export const retrieveValidShareLinkByReferer = async(referer: ValidReferer): Promise<ShareLinkDocument | null> => {
+  const ShareLink = getModelSafely<ShareLinkDocument, ShareLinkModel>('ShareLink');
+  if (ShareLink == null) {
+    logger.warn('Could not get ShareLink model. next() will be called without processing anything.');
+    return null;
+  }
+
+  const shareLinkId = referer;
+  const shareLink = await ShareLink.findOne({
+    id: shareLinkId,
+  });
+  if (shareLink == null || shareLink.isExpired()) {
+    logger.info(`ShareLink ('${shareLinkId}') is not found or has already expired.`);
+    return null;
+  }
+
+  return shareLink;
+};

+ 25 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-attachment.ts

@@ -0,0 +1,25 @@
+import { getIdForRef, type IAttachment } from '@growi/core';
+
+import { ShareLinkDocument } from '~/server/models/share-link';
+import { getModelSafely } from '~/server/util/mongoose-utils';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:middleware:certify-shared-page-attachment:validate-attachment');
+
+
+export const validateAttachment = async(fileId: string, shareLink: ShareLinkDocument): Promise<boolean> => {
+  const Attachment = getModelSafely<IAttachment>('Attachment');
+  if (Attachment == null) {
+    logger.warn('Could not get Attachment model. next() will be called without processing anything.');
+    return false;
+  }
+
+  const relatedPageId = getIdForRef(shareLink.relatedPage);
+  const result = await Attachment.exists({
+    _id: fileId,
+    page: relatedPageId,
+  });
+
+  return result != null;
+};

+ 1 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/index.ts

@@ -0,0 +1 @@
+export * from './validate-referer';

+ 59 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.spec.ts

@@ -0,0 +1,59 @@
+import { retrieveSiteUrl } from './retrieve-site-url';
+
+const mocks = vi.hoisted(() => {
+  return {
+    configManagerMock: {
+      getConfig: vi.fn(),
+    },
+  };
+});
+
+vi.mock('~/server/service/config-manager', () => {
+  return { configManager: mocks.configManagerMock };
+});
+
+
+describe('retrieveSiteUrl', () => {
+
+  describe('returns null', () => {
+
+    it('when the siteUrl is not set', () => {
+      // setup
+      mocks.configManagerMock.getConfig.mockImplementation(() => {
+        return null;
+      });
+
+      // when
+      const result = retrieveSiteUrl();
+
+      // then
+      expect(result).toBeNull();
+    });
+
+    it('when the siteUrl is invalid', () => {
+      // setup
+      mocks.configManagerMock.getConfig.mockImplementation(() => {
+        return 'invalid siteUrl string';
+      });
+
+      // when
+      const result = retrieveSiteUrl();
+
+      // then
+      expect(result).toBeNull();
+    });
+  });
+
+  it('returns a URL instance', () => {
+    // setup
+    const siteUrl = 'https://example.com';
+    mocks.configManagerMock.getConfig.mockImplementation(() => siteUrl);
+
+    // when
+    const result = retrieveSiteUrl();
+
+    // then
+    expect(result).toEqual(new URL(siteUrl));
+  });
+
+});

+ 22 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/retrieve-site-url.ts

@@ -0,0 +1,22 @@
+import { configManager } from '~/server/service/config-manager';
+import loggerFactory from '~/utils/logger';
+
+
+const logger = loggerFactory('growi:middlewares:certify-shared-file:validate-referer:retrieve-site-url');
+
+
+export const retrieveSiteUrl = (): URL | null => {
+  const siteUrlString = configManager.getConfig('crowi', 'app:siteUrl');
+  if (siteUrlString == null) {
+    logger.warn("Verification referer does not work because 'Site URL' is NOT set. All of attachments in share link page is invisible.");
+    return null;
+  }
+
+  try {
+    return new URL(siteUrlString);
+  }
+  catch (err) {
+    logger.error(`Parsing 'app:siteUrl' ('${siteUrlString}') has failed.`);
+    return null;
+  }
+};

+ 131 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.spec.ts

@@ -0,0 +1,131 @@
+import { objectIdUtils } from '@growi/core/dist/utils';
+
+import { validateReferer } from './validate-referer';
+
+const mocks = vi.hoisted(() => {
+  return {
+    retrieveSiteUrlMock: vi.fn(),
+  };
+});
+
+vi.mock('./retrieve-site-url', () => ({ retrieveSiteUrl: mocks.retrieveSiteUrlMock }));
+
+
+describe('validateReferer', () => {
+
+  const isValidObjectIdSpy = vi.spyOn(objectIdUtils, 'isValidObjectId');
+
+  beforeEach(() => {
+    isValidObjectIdSpy.mockClear();
+  });
+
+  describe('refurns false', () => {
+
+    it('when the referer argument is undefined', () => {
+      // setup
+
+      // when
+      const result = validateReferer(undefined);
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).not.toHaveBeenCalled();
+      expect(isValidObjectIdSpy).not.toHaveBeenCalled();
+    });
+
+    it('when the referer is invalid', () => {
+      // when
+      const result = validateReferer('invalid URL');
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).not.toHaveBeenCalledOnce();
+      expect(isValidObjectIdSpy).not.toHaveBeenCalled();
+    });
+
+    it('when the siteUrl returns null', () => {
+      // setup
+      mocks.retrieveSiteUrlMock.mockImplementation(() => {
+        return null;
+      });
+
+      // when
+      const refererString = 'https://example.org/share/xxxxx';
+      const result = validateReferer(refererString);
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).toHaveBeenCalledOnce();
+      expect(isValidObjectIdSpy).not.toHaveBeenCalled();
+    });
+
+    it('when the hostname of the referer does not match with siteUrl', () => {
+      // setup
+      mocks.retrieveSiteUrlMock.mockImplementation(() => {
+        return new URL('https://example.com');
+      });
+
+      // when
+      const refererString = 'https://example.org/share/xxxxx';
+      const result = validateReferer(refererString);
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).toHaveBeenCalledOnce();
+      expect(isValidObjectIdSpy).not.toHaveBeenCalled();
+    });
+
+    it('when the port of the referer does not match with siteUrl', () => {
+      // setup
+      mocks.retrieveSiteUrlMock.mockImplementation(() => {
+        return new URL('https://example.com');
+      });
+
+      // when
+      const refererString = 'https://example.com:8080/share/xxxxx';
+      const result = validateReferer(refererString);
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).toHaveBeenCalledOnce();
+      expect(isValidObjectIdSpy).not.toHaveBeenCalled();
+    });
+
+    it('when the shareLinkId is invalid', () => {
+      // setup
+      mocks.retrieveSiteUrlMock.mockImplementation(() => {
+        return new URL('https://example.com');
+      });
+
+      // when
+      const refererString = 'https://example.com/share/FFFFFFFFFFFFFFFFFFFFFFFF';
+      const result = validateReferer(refererString);
+
+      // then
+      expect(result).toBeFalsy();
+      expect(mocks.retrieveSiteUrlMock).toHaveBeenCalledOnce();
+      expect(isValidObjectIdSpy).toHaveBeenCalledOnce();
+    });
+
+  });
+
+  it('returns ValidReferer instance', () => {
+    // setup
+    mocks.retrieveSiteUrlMock.mockImplementation(() => {
+      return new URL('https://example.com');
+    });
+
+
+    // when
+    const shareLinkId = '65436ba09ae6983bd608b89c';
+    const refererString = `https://example.com/share/${shareLinkId}`;
+    const result = validateReferer(refererString);
+
+    // then
+    expect(result).toStrictEqual({
+      referer: refererString,
+      shareLinkId,
+    });
+  });
+
+});

+ 69 - 0
apps/app/src/server/middlewares/certify-shared-page-attachment/validate-referer/validate-referer.ts

@@ -0,0 +1,69 @@
+import { objectIdUtils } from '@growi/core/dist/utils';
+
+import loggerFactory from '~/utils/logger';
+
+import { ValidReferer } from '../interfaces';
+
+import { retrieveSiteUrl } from './retrieve-site-url';
+
+
+const logger = loggerFactory('growi:middlewares:certify-shared-file:validate-referer');
+
+
+export const validateReferer = (referer: string | undefined): ValidReferer | false => {
+  // not null
+  if (referer == null) {
+    logger.info('The referer string is undefined');
+    return false;
+  }
+
+  let refererUrl: URL;
+  try {
+    refererUrl = new URL(referer);
+  }
+  catch (err) {
+    logger.info(`Parsing referer ('${referer}') has failed`);
+    return false;
+  }
+
+  // siteUrl
+  const siteUrl = retrieveSiteUrl();
+  if (siteUrl == null) {
+    logger.info('The siteUrl is null.');
+    return false;
+  }
+
+  // validate hostname and port
+  if (refererUrl.hostname !== siteUrl.hostname || refererUrl.port !== siteUrl.port) {
+    logger.warn('The hostname or port mismatched.', {
+      refererUrl: {
+        hostname: refererUrl.hostname,
+        port: refererUrl.port,
+      },
+      siteUrl: {
+        hostname: siteUrl.hostname,
+        port: siteUrl.port,
+      },
+    });
+    return false;
+  }
+
+  // validate pathname
+  // https://regex101.com/r/M5Bp6E/1
+  const match = refererUrl.pathname.match(/^\/share\/(?<shareLinkId>[a-f0-9]{24})$/i);
+  if (match == null || match.groups?.shareLinkId == null) {
+    logger.warn(`The pathname ('${refererUrl.pathname}') is invalid.`, match);
+    return false;
+  }
+
+  // validate shareLinkId is an correct ObjectId
+  if (!objectIdUtils.isValidObjectId(match.groups.shareLinkId)) {
+    logger.warn(`The shareLinkId ('${match.groups.shareLinkId}') is invalid as an ObjectId.`);
+    return false;
+  }
+
+  return {
+    referer,
+    shareLinkId: match.groups.shareLinkId,
+  };
+};

+ 1 - 1
apps/app/src/server/middlewares/certify-shared-page.js

@@ -1,3 +1,4 @@
+import ShareLink from '~/server/models/share-link';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:middleware:certify-shared-page');
@@ -11,7 +12,6 @@ module.exports = (crowi) => {
       return next();
     }
 
-    const ShareLink = crowi.model('ShareLink');
     const sharelink = await ShareLink.findOne({ _id: shareLinkId, relatedPage: pageId });
 
     // check sharelink enabled

+ 0 - 1
apps/app/src/server/models/index.js

@@ -11,6 +11,5 @@ module.exports = {
   GlobalNotificationSetting: require('./GlobalNotificationSetting'),
   GlobalNotificationMailSetting: require('./GlobalNotificationSetting/GlobalNotificationMailSetting'),
   GlobalNotificationSlackSetting: require('./GlobalNotificationSetting/GlobalNotificationSlackSetting'),
-  ShareLink: require('./share-link'),
   SlackAppIntegration: require('./slack-app-integration'),
 };

+ 0 - 39
apps/app/src/server/models/share-link.js

@@ -1,39 +0,0 @@
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-
-const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate-v2');
-const uniqueValidator = require('mongoose-unique-validator');
-
-const ObjectId = mongoose.Schema.Types.ObjectId;
-
-/*
- * define schema
- */
-const schema = new mongoose.Schema({
-  relatedPage: {
-    type: ObjectId,
-    ref: 'Page',
-    required: true,
-    index: true,
-  },
-  expiredAt: { type: Date },
-  description: { type: String },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
-schema.plugin(mongoosePaginate);
-schema.plugin(uniqueValidator);
-
-module.exports = function(crowi) {
-
-  schema.methods.isExpired = function() {
-    if (this.expiredAt == null) {
-      return false;
-    }
-    return this.expiredAt.getTime() < new Date().getTime();
-  };
-
-  const model = mongoose.model('ShareLink', schema);
-  return model;
-};

+ 46 - 0
apps/app/src/server/models/share-link.ts

@@ -0,0 +1,46 @@
+import mongoose, { Schema } from 'mongoose';
+import type {
+  Document, Model,
+} from 'mongoose';
+import mongoosePaginate from 'mongoose-paginate-v2';
+import uniqueValidator from 'mongoose-unique-validator';
+
+import type { IShareLink } from '~/interfaces/share-link';
+
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+
+export interface ShareLinkDocument extends IShareLink, Document {
+  isExpired: () => boolean,
+}
+
+export type ShareLinkModel = Model<ShareLinkDocument>;
+
+
+/*
+ * define schema
+ */
+const ObjectId = mongoose.Schema.Types.ObjectId;
+const schema = new Schema<ShareLinkDocument, ShareLinkModel>({
+  relatedPage: {
+    type: ObjectId,
+    ref: 'Page',
+    required: true,
+    index: true,
+  },
+  expiredAt: { type: Date },
+  description: { type: String },
+}, {
+  timestamps: { createdAt: true, updatedAt: false },
+});
+schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
+
+schema.methods.isExpired = function() {
+  if (this.expiredAt == null) {
+    return false;
+  }
+  return this.expiredAt.getTime() < new Date().getTime();
+};
+
+export default getOrCreateModel<ShareLinkDocument, ShareLinkModel>('ShareLink', schema);

+ 1 - 2
apps/app/src/server/routes/apiv3/security-settings/index.js

@@ -5,6 +5,7 @@ import { SupportedAction } from '~/interfaces/activity';
 import { PageDeleteConfigValue } from '~/interfaces/page-delete-config';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import ShareLink from '~/server/models/share-link';
 import { configManager } from '~/server/service/config-manager';
 import loggerFactory from '~/utils/logger';
 import { validateDeleteConfigs, prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
@@ -731,7 +732,6 @@ module.exports = (crowi) => {
    *                      description: suceed to get all share links
    */
   router.get('/all-share-links/', loginRequiredStrictly, adminRequired, async(req, res) => {
-    const ShareLink = crowi.model('ShareLink');
     const page = parseInt(req.query.page) || 1;
     const limit = 10;
     const linkQuery = {};
@@ -769,7 +769,6 @@ module.exports = (crowi) => {
    */
 
   router.delete('/all-share-links/', loginRequiredStrictly, adminRequired, async(req, res) => {
-    const ShareLink = crowi.model('ShareLink');
     try {
       const removedAct = await ShareLink.remove({});
       const removeTotal = await removedAct.n;

+ 2 - 5
apps/app/src/server/routes/apiv3/share-links.js

@@ -1,17 +1,17 @@
 // TODO remove this setting after implemented all
 /* eslint-disable no-unused-vars */
 import { ErrorV3 } from '@growi/core/dist/models';
+import express from 'express';
 
 import { SupportedAction } from '~/interfaces/activity';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
+import ShareLink from '~/server/models/share-link';
 import loggerFactory from '~/utils/logger';
 
 const logger = loggerFactory('growi:routes:apiv3:share-links');
 
-const express = require('express');
-
 const router = express.Router();
 
 const { body, query, param } = require('express-validator');
@@ -31,7 +31,6 @@ module.exports = (crowi) => {
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
-  const ShareLink = crowi.model('ShareLink');
   const Page = crowi.model('Page');
 
   const activityEvent = crowi.event('activity');
@@ -145,8 +144,6 @@ module.exports = (crowi) => {
       return res.apiv3Err(new ErrorV3(msg, 'post-shareLink-failed'));
     }
 
-    const ShareLink = crowi.model('ShareLink');
-
     try {
       const postedShareLink = await ShareLink.create({ relatedPage, expiredAt, description });
 

+ 2 - 2
apps/app/src/server/routes/index.js

@@ -6,6 +6,7 @@ import { middlewareFactory as rateLimiterFactory } from '~/features/rate-limiter
 import { generateAddActivityMiddleware } from '../middlewares/add-activity';
 import apiV1FormValidator from '../middlewares/apiv1-form-validator';
 import { generateCertifyBrandLogoMiddleware } from '../middlewares/certify-brand-logo';
+import { certifySharedPageAttachmentMiddleware } from '../middlewares/certify-shared-page-attachment';
 import { excludeReadOnlyUser } from '../middlewares/exclude-read-only-user';
 import injectResetOrderByTokenMiddleware from '../middlewares/inject-reset-order-by-token-middleware';
 import injectUserRegistrationOrderByTokenMiddleware from '../middlewares/inject-user-registration-order-by-token-middleware';
@@ -33,7 +34,6 @@ module.exports = function(crowi, app) {
   const loginRequiredStrictly = require('../middlewares/login-required')(crowi);
   const loginRequired = require('../middlewares/login-required')(crowi, true);
   const adminRequired = require('../middlewares/admin-required')(crowi);
-  const certifySharedFile = require('../middlewares/certify-shared-file')(crowi);
   const certifyBrandLogo = generateCertifyBrandLogoMiddleware(crowi);
   const addActivity = generateAddActivityMiddleware(crowi);
 
@@ -153,7 +153,7 @@ module.exports = function(crowi, app) {
 
   app.get('/me'                                   , loginRequiredStrictly, next.delegateToNext);
   app.get('/me/*'                                 , loginRequiredStrictly, next.delegateToNext);
-  app.get('/attachment/:id([0-9a-z]{24})' , certifySharedFile , loginRequired, attachment.api.get);
+  app.get('/attachment/:id([0-9a-z]{24})'         , certifySharedPageAttachmentMiddleware , loginRequired, attachment.api.get);
   app.get('/attachment/profile/:id([0-9a-z]{24})' , loginRequired, attachment.api.get);
   app.get('/attachment/:pageId/:fileName'       , loginRequired, attachment.api.obsoletedGetForMongoDB); // DEPRECATED: remains for backward compatibility for v3.3.x or below
   app.get('/download/:id([0-9a-z]{24})'         , loginRequired, attachment.api.download);

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

@@ -37,6 +37,7 @@ import { IOptionsForCreate, IOptionsForUpdate } from '../models/interfaces/page-
 import PageOperation, { PageOperationDocument } from '../models/page-operation';
 import { PageRedirectModel } from '../models/page-redirect';
 import { serializePageSecurely } from '../models/serializers/page-serializer';
+import ShareLink from '../models/share-link';
 import Subscription from '../models/subscription';
 import UserGroupRelation from '../models/user-group-relation';
 import { V5ConversionError } from '../models/vo/v5-conversion-error';
@@ -1693,7 +1694,6 @@ class PageService {
     const Comment = this.crowi.model('Comment');
     const Page = this.crowi.model('Page');
     const PageTagRelation = this.crowi.model('PageTagRelation');
-    const ShareLink = this.crowi.model('ShareLink');
     const Revision = this.crowi.model('Revision');
     const Attachment = this.crowi.model('Attachment');
     const PageRedirect = mongoose.model('PageRedirect') as unknown as PageRedirectModel;

+ 2 - 2
apps/app/src/server/util/mongoose-utils.ts

@@ -18,9 +18,9 @@ export const getMongoUri = (): string => {
     || ((env.NODE_ENV === 'test') ? 'mongodb://mongo/growi_test' : 'mongodb://mongo/growi');
 };
 
-export const getModelSafely = <T>(modelName: string): Model<T & Document> | null => {
+export const getModelSafely = <Interface, Method = Interface>(modelName: string): Method & Model<Interface & Document> | null => {
   if (mongoose.modelNames().includes(modelName)) {
-    return mongoose.model<T & Document>(modelName);
+    return mongoose.model<Interface & Document, Method & Model<Interface & Document>>(modelName);
   }
   return null;
 };

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

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "6.2.2-slackbot-proxy.0",
+  "version": "6.2.3-slackbot-proxy.0",
   "license": "MIT",
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/core",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/hackmd/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/hackmd",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI js and css files to use hackmd",
   "license": "MIT",
   "type": "module",

+ 1 - 1
packages/presentation/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/presentation",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI plugin for presentation",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/preset-templates/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/preset-templates",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "scripts": {
     "test": "vitest run",
     "version": "yarn version --no-git-tag-version --preid=RC"

+ 1 - 1
packages/preset-themes/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@growi/preset-themes",
   "description": "GROWI preset themes",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "license": "MIT",
   "main": "dist/libs/preset-themes.umd.js",
   "module": "dist/libs/preset-themes.mjs",

+ 1 - 1
packages/remark-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-attachment-refs",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-drawio/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-drawio",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "remark plugin to draw diagrams with draw.io (diagrams.net)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-growi-directive/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-growi-directive",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "remark plugin to support GROWI plugin (forked from remark-directive@2.0.1)",
   "license": "MIT",
   "keywords": [

+ 1 - 1
packages/remark-lsx/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/remark-lsx",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "keywords": [

+ 11 - 2
packages/remark-lsx/src/client/components/LsxPageList/LsxPage.tsx

@@ -97,8 +97,17 @@ export const LsxPage = React.memo((props: Props): JSX.Element => {
     if (pageNode.page == null) {
       return <></>;
     }
-    return <PageListMeta page={pageNode.page} basisViewersCount={basisViewersCount} />;
-  }, [basisViewersCount, pageNode.page]);
+
+    const { page } = pageNode;
+
+    return (
+      <PageListMeta
+        page={page}
+        basisViewersCount={basisViewersCount}
+        likerCount={page.liker.length}
+      />
+    );
+  }, [basisViewersCount, pageNode]);
 
   return (
     <li className={`page-list-li ${styles['page-list-li']}`}>

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slack",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "license": "MIT",
   "type": "module",
   "main": "dist/index.cjs",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/ui",
-  "version": "6.2.2-RC.0",
+  "version": "6.2.3-RC.0",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "keywords": [

+ 11 - 11
yarn.lock

@@ -2295,13 +2295,13 @@
     xdg-basedir "^4.0.0"
 
 "@growi/core@link:packages/core":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     bson-objectid "^2.0.4"
     escape-string-regexp "^4.0.0"
 
 "@growi/hackmd@link:packages/hackmd":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
 
 "@growi/pluginkit@link:packages/pluginkit":
   version "0.1.0"
@@ -2310,18 +2310,18 @@
     extensible-custom-error "^0.0.7"
 
 "@growi/presentation@link:packages/presentation":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
 
 "@growi/preset-templates@link:packages/preset-templates":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
 
 "@growi/preset-themes@link:packages/preset-themes":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
 
 "@growi/remark-attachment-refs@link:packages/remark-attachment-refs":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2333,10 +2333,10 @@
     universal-bunyan "^0.9.2"
 
 "@growi/remark-drawio@link:packages/remark-drawio":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
 
 "@growi/remark-growi-directive@link:packages/remark-growi-directive":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@types/mdast" "^3.0.0"
     "@types/unist" "^2.0.0"
@@ -2353,7 +2353,7 @@
     uvu "^0.5.0"
 
 "@growi/remark-lsx@link:packages/remark-lsx":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"
     "@growi/remark-growi-directive" "link:packages/remark-growi-directive"
@@ -2365,7 +2365,7 @@
     swr "^2.0.3"
 
 "@growi/slack@link:packages/slack":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@slack/oauth" "^2.0.1"
     "@slack/web-api" "^6.2.4"
@@ -2381,7 +2381,7 @@
     url-join "^4.0.0"
 
 "@growi/ui@link:packages/ui":
-  version "6.2.2-RC.0"
+  version "6.2.3-RC.0"
   dependencies:
     "@growi/core" "link:packages/core"