Sfoglia il codice sorgente

Merge branch 'master' into imprv/148445-upgrade-remark-growi-directive

reiji-h 1 anno fa
parent
commit
cb59b3e654

+ 1 - 1
apps/app/package.json

@@ -204,7 +204,7 @@
     "uglifycss": "^0.0.29",
     "universal-bunyan": "^0.9.2",
     "unstated": "^2.1.1",
-    "unzip-stream": "^0.3.1",
+    "unzip-stream": "^0.3.2",
     "url-join": "^4.0.0",
     "usehooks-ts": "^2.6.0",
     "validator": "^13.7.0",

+ 0 - 36
apps/app/src/client/components/SearchForm.module.scss

@@ -1,36 +0,0 @@
-@use '@growi/core-styles/scss/bootstrap/init' as bs;
-
-.grw-search-table {
-  caption {
-    display: table-header-group;
-  }
-}
-
-@include bs.media-breakpoint-down(sm) {
-  .grw-search-table {
-    th {
-      text-align: right;
-    }
-
-    td {
-      overflow-wrap: anywhere;
-      white-space: normal !important;
-    }
-
-    @include bs.media-breakpoint-down(xs) {
-      th,
-      td {
-        display: block;
-      }
-
-      th {
-        text-align: left;
-      }
-
-      td {
-        padding-top: 0 !important;
-        border-top: none !important;
-      }
-    }
-  }
-}

+ 0 - 143
apps/app/src/client/components/SearchForm.tsx

@@ -1,143 +0,0 @@
-import React, {
-  FC, forwardRef, ForwardRefRenderFunction, useImperativeHandle,
-  useRef, useState,
-} from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { IFocusable } from '~/client/interfaces/focusable';
-import { TypeaheadProps } from '~/client/interfaces/react-bootstrap-typeahead';
-import { IPageWithSearchMeta } from '~/interfaces/search';
-
-import SearchTypeahead from './SearchTypeahead';
-
-import styles from './SearchForm.module.scss';
-
-
-type SearchFormHelpProps = {
-  isReachable: boolean,
-}
-
-const SearchFormHelp: FC<SearchFormHelpProps> = React.memo((props: SearchFormHelpProps) => {
-  const { t } = useTranslation();
-
-  const { isReachable } = props;
-
-  if (!isReachable) {
-    return (
-      <>
-        <h5 className="text-danger">Error occured on Search Service</h5>
-        Try to reconnect from management page.
-      </>
-    );
-  }
-
-  return (
-    <table className={`${styles['grw-search-table']} table grw-search-table search-help m-0`}>
-      <caption className="text-start text-primary p-2">
-        <h5 className="h6"><span className="material-symbols-outlined">search</span>{ t('search_help.title') }</h5>
-      </caption>
-      <tbody>
-        <tr>
-          <th className="py-2">
-            <code>word1</code> <code>word2</code><br></br>
-            <small>({ t('search_help.and.syntax help') })</small>
-          </th>
-          <td><h6 className="m-0">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2">
-            <code>&quot;This is GROWI&quot;</code><br></br>
-            <small>({ t('search_help.phrase.syntax help') })</small>
-          </th>
-          <td><h6 className="m-0">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-keyword</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>prefix:/user/</code></th>
-          <td><h6 className="m-0">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-prefix:/user/</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>tag:wiki</code></th>
-          <td><h6 className="m-0">{ t('search_help.tag.desc', { tag: 'wiki' }) }</h6></td>
-        </tr>
-        <tr>
-          <th className="py-2"><code>-tag:wiki</code></th>
-          <td><h6 className="m-0">{ t('search_help.exclude_tag.desc', { tag: 'wiki' }) }</h6></td>
-        </tr>
-      </tbody>
-    </table>
-  );
-});
-
-SearchFormHelp.displayName = 'SearchFormHelp';
-
-
-type Props = TypeaheadProps & {
-  isSearchServiceReachable: boolean,
-
-  keywordOnInit?: string,
-  disableIncrementalSearch?: boolean,
-  onChange?: (data: IPageWithSearchMeta[]) => void,
-  onSubmit?: (input: string) => void,
-};
-
-
-const SearchForm: ForwardRefRenderFunction<IFocusable, Props> = (props: Props, ref) => {
-  const { t } = useTranslation();
-  const {
-    isSearchServiceReachable,
-    keywordOnInit,
-    disableIncrementalSearch,
-    dropup, onChange, onBlur, onFocus, onSubmit, onInputChange,
-  } = props;
-
-  const [searchError, setSearchError] = useState<Error | null>(null);
-
-  const searchTyheaheadRef = useRef<IFocusable>(null);
-
-  // publish focus()
-  useImperativeHandle(ref, () => ({
-    focus() {
-      const instance = searchTyheaheadRef?.current;
-      if (instance != null) {
-        instance.focus();
-      }
-    },
-  }));
-
-  const placeholder = isSearchServiceReachable
-    ? 'Search ...'
-    : 'Error on Search Service';
-
-  const emptyLabel = (searchError != null)
-    ? 'Error on searching.'
-    : t('search.search page bodies');
-
-  return (
-    <SearchTypeahead
-      ref={searchTyheaheadRef}
-      dropup={dropup}
-      emptyLabel={emptyLabel}
-      placeholder={placeholder}
-      onChange={onChange}
-      onSubmit={onSubmit}
-      onInputChange={onInputChange}
-      onSearchError={err => setSearchError(err)}
-      onBlur={onBlur}
-      onFocus={onFocus}
-      keywordOnInit={keywordOnInit}
-      disableIncrementalSearch={disableIncrementalSearch}
-      helpElement={<SearchFormHelp isReachable={isSearchServiceReachable} />}
-    />
-  );
-};
-
-export default forwardRef(SearchForm);

+ 23 - 5
apps/app/src/server/models/page.ts

@@ -65,6 +65,18 @@ type PaginatedPages = {
   offset: number
 }
 
+export type FindRecentUpdatedPagesOption = {
+  offset: number,
+  limit: number,
+  includeWipPage: boolean,
+  includeTrashed: boolean,
+  isRegExpEscapedFromPath: boolean,
+  sort: 'updatedAt'
+  desc: number
+  hideRestrictedByOwner: boolean,
+  hideRestrictedByGroup: boolean,
+}
+
 export type CreateMethod = (path: string, body: string, user, options: IOptionsForCreate) => Promise<HydratedDocument<PageDocument>>
 
 export interface PageModel extends Model<PageDocument> {
@@ -79,7 +91,7 @@ export interface PageModel extends Model<PageDocument> {
   countByPathAndViewer(path: string | null, user, userGroups?, includeEmpty?:boolean): Promise<number>
   findParentByPath(path: string | null): Promise<HydratedDocument<PageDocument> | null>
   findTargetAndAncestorsByPathOrId(pathOrId: string): Promise<TargetAndAncestorsResult>
-  findRecentUpdatedPages(path: string, user, option, includeEmpty?: boolean): Promise<PaginatedPages>
+  findRecentUpdatedPages(path: string, user, option: FindRecentUpdatedPagesOption, includeEmpty?: boolean): Promise<PaginatedPages>
   generateGrantCondition(
     user, userGroups: ObjectIdLike[] | null, includeAnyoneWithTheLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): { $or: any[] }
@@ -414,13 +426,19 @@ export class PageQueryBuilder {
   }
 
   // add viewer condition to PageQueryBuilder instance
-  async addViewerCondition(user, userGroups = null, includeAnyoneWithTheLink = false): Promise<PageQueryBuilder> {
+  async addViewerCondition(
+      user,
+      userGroups = null,
+      includeAnyoneWithTheLink = false,
+      showPagesRestrictedByOwner = false,
+      showPagesRestrictedByGroup = false,
+  ): Promise<PageQueryBuilder> {
     const relatedUserGroups = (user != null && userGroups == null) ? [
       ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
       ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
     ] : userGroups;
 
-    this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink);
+    this.addConditionToFilteringByViewer(user, relatedUserGroups, includeAnyoneWithTheLink, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
     return this;
   }
 
@@ -664,7 +682,7 @@ schema.statics.countByPathAndViewer = async function(path: string | null, user,
 };
 
 schema.statics.findRecentUpdatedPages = async function(
-    path: string, user, options, includeEmpty = false,
+    path: string, user, options: FindRecentUpdatedPagesOption, includeEmpty = false,
 ): Promise<PaginatedPages> {
 
   const sortOpt = {};
@@ -690,7 +708,7 @@ schema.statics.findRecentUpdatedPages = async function(
 
   queryBuilder.addConditionToListWithDescendants(path, options);
   queryBuilder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
-  await queryBuilder.addViewerCondition(user);
+  await queryBuilder.addViewerCondition(user, undefined, undefined, !options.hideRestrictedByOwner, !options.hideRestrictedByGroup);
   const pages = await Page.paginate(queryBuilder.query.clone(), {
     lean: true, sort: sortOpt, offset: options.offset, limit: options.limit,
   });

+ 8 - 1
apps/app/src/server/routes/apiv3/page-listing.ts

@@ -9,6 +9,7 @@ import { query, oneOf } from 'express-validator';
 import type { HydratedDocument } from 'mongoose';
 import mongoose from 'mongoose';
 
+import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
 import loggerFactory from '~/utils/logger';
 
@@ -18,6 +19,7 @@ import type { PageDocument, PageModel } from '../../models/page';
 
 import type { ApiV3Response } from './interfaces/apiv3-response';
 
+
 const logger = loggerFactory('growi:routes:apiv3:page-tree');
 
 /*
@@ -103,8 +105,13 @@ const routerFactory = (crowi: Crowi): Router => {
 
     const pageService = crowi.pageService;
 
+    const hideRestrictedByOwner = await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const hideRestrictedByGroup = await configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
+
     try {
-      const pages = await pageService.findChildrenByParentPathOrIdAndViewer((id || path)as string, req.user);
+      const pages = await pageService.findChildrenByParentPathOrIdAndViewer(
+        (id || path)as string, req.user, undefined, !hideRestrictedByOwner, !hideRestrictedByGroup,
+      );
       return res.apiv3({ children: pages });
     }
     catch (err) {

+ 9 - 0
apps/app/src/server/routes/apiv3/pages/index.js

@@ -226,6 +226,12 @@ module.exports = (crowi) => {
     const offset = parseInt(req.query.offset) || 0;
     const includeWipPage = req.query.includeWipPage === 'true'; // Need validation using express-validator
 
+    const hideRestrictedByOwner = await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner');
+    const hideRestrictedByGroup = await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup');
+
+    /**
+    * @type {import('~/server/models/page').FindRecentUpdatedPagesOption}
+    */
     const queryOptions = {
       offset,
       limit,
@@ -234,7 +240,10 @@ module.exports = (crowi) => {
       isRegExpEscapedFromPath: true,
       sort: 'updatedAt',
       desc: -1,
+      hideRestrictedByOwner,
+      hideRestrictedByGroup,
     };
+
     try {
       const result = await Page.findRecentUpdatedPages('/', req.user, queryOptions);
       if (result.pages.length > limit) {

+ 8 - 3
apps/app/src/server/service/page/index.ts

@@ -4332,8 +4332,13 @@ class PageService implements IPageService {
   /*
    * Find all children by parent's path or id. Using id should be prioritized
    */
-  async findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups = null)
-      : Promise<(HydratedDocument<PageDocument> & { processData?: IPageOperationProcessData })[]> {
+  async findChildrenByParentPathOrIdAndViewer(
+      parentPathOrId: string,
+      user,
+      userGroups = null,
+      showPagesRestrictedByOwner = false,
+      showPagesRestrictedByGroup = false,
+  ): Promise<(HydratedDocument<PageDocument> & { processData?: IPageOperationProcessData })[]> {
     const Page = mongoose.model<HydratedDocument<PageDocument>, PageModel>('Page');
     let queryBuilder: PageQueryBuilder;
     if (hasSlash(parentPathOrId)) {
@@ -4346,7 +4351,7 @@ class PageService implements IPageService {
       // Use $eq for user-controlled sources. see: https://codeql.github.com/codeql-query-help/javascript/js-sql-injection/#recommendation
       queryBuilder = new PageQueryBuilder(Page.find({ parent: { $eq: parentId } } as any), true); // TODO: improve type
     }
-    await queryBuilder.addViewerCondition(user, userGroups);
+    await queryBuilder.addViewerCondition(user, userGroups, undefined, showPagesRestrictedByOwner, showPagesRestrictedByGroup);
 
     const pages: HydratedDocument<PageDocument>[] = await queryBuilder
       .addConditionToSortPagesByAscPath()

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

@@ -21,7 +21,9 @@ export interface IPageService {
   getEventEmitter: () => EventEmitter,
   deleteMultipleCompletely: (pages: ObjectIdLike[], user: IUser | undefined) => Promise<void>,
   findAncestorsChildrenByPathAndViewer(path: string, user, userGroups?): Promise<Record<string, PageDocument[]>>,
-  findChildrenByParentPathOrIdAndViewer(parentPathOrId: string, user, userGroups?): Promise<PageDocument[]>,
+  findChildrenByParentPathOrIdAndViewer(
+    parentPathOrId: string, user, userGroups?, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
+  ): Promise<PageDocument[]>,
   shortBodiesMapByPageIds(pageIds?: Types.ObjectId[], user?): Promise<Record<string, string | null>>,
   constructBasicPageInfo(page: PageDocument, isGuestUser?: boolean): IPageInfo | Omit<IPageInfoForEntity, 'bookmarkCount'>,
   normalizeAllPublicPages(): Promise<void>,

+ 1 - 0
packages/editor/src/client/components/CodeMirrorEditorReadOnly.tsx

@@ -13,6 +13,7 @@ const additionalExtensions: Extension[] = [
   [
     setDataLine,
     EditorState.readOnly.of(true),
+    EditorView.editable.of(false),
   ],
 ];
 

+ 1 - 1
packages/editor/src/client/stores/codemirror-editor.ts

@@ -33,7 +33,7 @@ export const useCodeMirrorEditorIsolated = (
 
   const newData = useCodeMirrorEditor(mergedProps);
 
-  const shouldUpdate = swrKey != null && container != null && props != null && (
+  const shouldUpdate = swrKey != null && container != null && (
     currentData == null
     || (isValid(newData) && !isDeepEquals(currentData, newData))
   );

+ 4 - 4
yarn.lock

@@ -18757,10 +18757,10 @@ untildify@^4.0.0:
   resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
   integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
 
-unzip-stream@^0.3.1:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.1.tgz#2333b5cd035d29db86fb701ca212cf8517400083"
-  integrity sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==
+unzip-stream@^0.3.2:
+  version "0.3.2"
+  resolved "https://registry.yarnpkg.com/unzip-stream/-/unzip-stream-0.3.2.tgz#e7bfd887f4c9a284b46978693ba54e0d4f5095df"
+  integrity sha512-oWhfqwjx36ULFG+krfkbtbrc/BeEzaYrlqdEWa5EPNd6x6RerzuNW8aSTM0TtNtrOfUKYdO0TwrlkzrXAE6Olg==
   dependencies:
     binary "^0.3.0"
     mkdirp "^0.5.1"