Explorar el Código

Merge pull request #3544 from weseek/master

release v4.2.13
Yuki Takei hace 5 años
padre
commit
d76d78a862
Se han modificado 41 ficheros con 386 adiciones y 194 borrados
  1. 7 1
      CHANGES.md
  2. 2 1
      package.json
  3. 8 0
      resource/locales/en_US/admin/admin.json
  4. 8 0
      resource/locales/ja_JP/admin/admin.json
  5. 9 1
      resource/locales/zh_CN/admin/admin.json
  6. 3 0
      src/client/js/app.jsx
  7. 0 3
      src/client/js/base.jsx
  8. 115 0
      src/client/js/components/Admin/MarkdownSetting/IndentForm.jsx
  9. 8 0
      src/client/js/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx
  10. 2 2
      src/client/js/components/Navbar/PageEditorModeManager.jsx
  11. 5 0
      src/client/js/components/PageComment/CommentEditor.jsx
  12. 10 0
      src/client/js/components/PageEditor.jsx
  13. 1 1
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  14. 1 0
      src/client/js/components/PageEditor/Editor.jsx
  15. 39 1
      src/client/js/components/PageEditor/OptionsSelector.jsx
  16. 4 1
      src/client/js/components/Sidebar/CustomSidebar.jsx
  17. 2 2
      src/client/js/components/TableOfContents.jsx
  18. 22 0
      src/client/js/services/AdminMarkDownContainer.js
  19. 1 7
      src/client/js/services/AdminUserGroupDetailContainer.js
  20. 1 0
      src/client/js/services/EditorContainer.js
  21. 0 49
      src/server/models/bookmark.js
  22. 4 0
      src/server/models/config.js
  23. 3 3
      src/server/models/page.js
  24. 0 57
      src/server/models/revision.js
  25. 1 5
      src/server/models/user-group-relation.js
  26. 2 2
      src/server/models/user-group.js
  27. 2 2
      src/server/models/user.js
  28. 4 5
      src/server/routes/apiv3/attachment.js
  29. 8 1
      src/server/routes/apiv3/bookmarks.js
  30. 29 0
      src/server/routes/apiv3/markdown-setting.js
  31. 15 0
      src/server/routes/apiv3/pages.js
  32. 14 5
      src/server/routes/apiv3/revisions.js
  33. 2 1
      src/server/routes/apiv3/user-group-relation.js
  34. 23 17
      src/server/routes/apiv3/user-group.js
  35. 8 8
      src/server/routes/comment.js
  36. 0 1
      src/server/routes/index.js
  37. 10 5
      src/server/routes/page.js
  38. 6 0
      src/server/routes/search.js
  39. 0 11
      src/server/routes/user.js
  40. 2 2
      src/server/service/page.js
  41. 5 0
      yarn.lock

+ 7 - 1
CHANGES.md

@@ -1,6 +1,12 @@
 # CHANGES
 # CHANGES
 
 
-## v4.2.12-RC
+## v4.2.13-RC
+
+* Feature: Detect indent size automatically
+* Fix: Some API responses includes email unintentionally
+* Fix: An error always displayed in admin pages
+
+## v4.2.12
 
 
 * Feature: Custom Sidebar
 * Feature: Custom Sidebar
 * Fix: Set language correctly for draw.io (diagrams.net)
 * Fix: Set language correctly for draw.io (diagrams.net)

+ 2 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.2.12-RC",
+  "version": "4.2.13-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -96,6 +96,7 @@
     "cross-env": "^7.0.0",
     "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
     "date-fns": "^2.0.0",
+    "detect-indent": "^6.0.0",
     "diff": "^4.0.1",
     "diff": "^4.0.1",
     "elasticsearch": "^16.0.0",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",
     "entities": "^2.0.0",

+ 8 - 0
resource/locales/en_US/admin/admin.json

@@ -78,6 +78,14 @@
       "enable_lineBreak_for_comment": "Enable line break in comment",
       "enable_lineBreak_for_comment": "Enable line break in comment",
       "enable_lineBreak_for_comment_desc": "Convert line break in comment to<code>&lt;br&gt;</code>in HTML"
       "enable_lineBreak_for_comment_desc": "Convert line break in comment to<code>&lt;br&gt;</code>in HTML"
     },
     },
+    "indent_header": "Indent setting",
+    "indent_desc": "You can change indent settings.",
+    "indent_options": {
+      "indentSize": "Default indent size",
+      "indentSize_desc": "Set the default indent size for the Markdown editor",
+      "disallow_indent_change": "Disallow change of indent size by users",
+      "disallow_indent_change_desc": "Force users to use ther default indent size."
+    },
     "presentation_header": "Presentation setting",
     "presentation_header": "Presentation setting",
     "presentation_desc": "You can change presentation settings.",
     "presentation_desc": "You can change presentation settings.",
     "presentation_options": {
     "presentation_options": {

+ 8 - 0
resource/locales/ja_JP/admin/admin.json

@@ -78,6 +78,14 @@
       "enable_lineBreak_for_comment": "コメント欄で Line Break を有効にする",
       "enable_lineBreak_for_comment": "コメント欄で Line Break を有効にする",
       "enable_lineBreak_for_comment_desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
       "enable_lineBreak_for_comment_desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
     },
     },
+    "indent_header": "インデント設定",
+    "indent_desc": "インデントの設定を変更できます。",
+    "indent_options": {
+      "indentSize": "既定のインデント幅",
+      "indentSize_desc": "既定のインデント幅を指定します。",
+      "disallow_indent_change": "ユーザによるインデント幅変更を許可しない",
+      "disallow_indent_change_desc": "ユーザにデフォルトのインデント幅の使用を強制します。"
+    },
     "presentation_header": "プレゼンテーション設定",
     "presentation_header": "プレゼンテーション設定",
     "presentation_desc": "プレゼンテーションの設定を変更できます。",
     "presentation_desc": "プレゼンテーションの設定を変更できます。",
     "presentation_options": {
     "presentation_options": {

+ 9 - 1
resource/locales/zh_CN/admin/admin.json

@@ -77,7 +77,15 @@
 			"enable_lineBreak_for_comment": "注释中启用换行符",
 			"enable_lineBreak_for_comment": "注释中启用换行符",
 			"enable_lineBreak_for_comment_desc": "HTML中将注释中的换行符转换为<code>&lt;br&gt;</code>"
 			"enable_lineBreak_for_comment_desc": "HTML中将注释中的换行符转换为<code>&lt;br&gt;</code>"
 		},
 		},
-		"presentation_header": "演示文稿设置",
+    "indent_header": "缩进设置",
+    "indent_desc": "您可以更改缩进设置。",
+    "indent_options": {
+      "indentSize": "默认的缩进值",
+      "indentSize_desc": "您可以更改Markdown编辑器的默认的缩进值。",
+      "disallow_indent_change": "不允许用户更改缩进值",
+      "disallow_indent_change_desc": "您可以不允许用户更改缩进值。"
+    },
+    "presentation_header": "演示文稿设置",
 		"presentation_desc": "您可以更改演示文稿设置。",
 		"presentation_desc": "您可以更改演示文稿设置。",
 		"presentation_options": {
 		"presentation_options": {
 			"page_break_setting": "分页设置",
 			"page_break_setting": "分页设置",

+ 3 - 0
src/client/js/app.jsx

@@ -6,6 +6,7 @@ import { I18nextProvider } from 'react-i18next';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
 import ErrorBoundary from './components/ErrorBoudary';
 import ErrorBoundary from './components/ErrorBoudary';
+import Sidebar from './components/Sidebar';
 import SearchPage from './components/SearchPage';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import TagsList from './components/TagsList';
 import DisplaySwitcher from './components/Page/DisplaySwitcher';
 import DisplaySwitcher from './components/Page/DisplaySwitcher';
@@ -79,6 +80,8 @@ logger.info('unstated containers have been initialized');
  *  value: React Element
  *  value: React Element
  */
  */
 Object.assign(componentMappings, {
 Object.assign(componentMappings, {
+  'grw-sidebar-wrapper': <Sidebar />,
+
   'search-page': <SearchPage crowi={appContainer} />,
   'search-page': <SearchPage crowi={appContainer} />,
 
 
   // 'revision-history': <PageHistory pageId={pageId} />,
   // 'revision-history': <PageHistory pageId={pageId} />,

+ 0 - 3
src/client/js/base.jsx

@@ -5,7 +5,6 @@ import Xss from '@commons/service/xss';
 
 
 import GrowiNavbar from './components/Navbar/GrowiNavbar';
 import GrowiNavbar from './components/Navbar/GrowiNavbar';
 import GrowiNavbarBottom from './components/Navbar/GrowiNavbarBottom';
 import GrowiNavbarBottom from './components/Navbar/GrowiNavbarBottom';
-import Sidebar from './components/Sidebar';
 import HotkeysManager from './components/Hotkeys/HotkeysManager';
 import HotkeysManager from './components/Hotkeys/HotkeysManager';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
@@ -42,8 +41,6 @@ const componentMappings = {
 
 
   'page-create-modal': <PageCreateModal />,
   'page-create-modal': <PageCreateModal />,
 
 
-  'grw-sidebar-wrapper': <Sidebar />,
-
   'grw-hotkeys-manager': <HotkeysManager />,
   'grw-hotkeys-manager': <HotkeysManager />,
 
 
 };
 };

+ 115 - 0
src/client/js/components/Admin/MarkdownSetting/IndentForm.jsx

@@ -0,0 +1,115 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+import {
+  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:importer');
+
+const IndentForm = (props) => {
+  const onClickSubmit = async(props) => {
+    const { t } = props;
+
+    try {
+      await props.adminMarkDownContainer.updateIndentSetting();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.indent_header') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  };
+
+  const renderIndentSizeOption = (props) => {
+    const { t, adminMarkDownContainer } = props;
+    const { adminPreferredIndentSize } = adminMarkDownContainer.state;
+
+    return (
+      <div className="col">
+        <div>
+          <label htmlFor="adminPreferredIndentSize">{t('admin:markdown_setting.indent_options.indentSize')}</label>
+          <UncontrolledDropdown id="adminPreferredIndentSize">
+            <DropdownToggle caret className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-right">
+              <span className="float-left">
+                {adminPreferredIndentSize || 4}
+              </span>
+            </DropdownToggle>
+            <DropdownMenu className="dropdown-menu" role="menu">
+              {[2, 4].map((num) => {
+                return (
+                  <DropdownItem key={num} role="presentation" onClick={() => adminMarkDownContainer.setAdminPreferredIndentSize(num)}>
+                    <a role="menuitem">{num}</a>
+                  </DropdownItem>
+                );
+              })}
+            </DropdownMenu>
+          </UncontrolledDropdown>
+        </div>
+        <p className="form-text text-muted">
+          {t('admin:markdown_setting.indent_options.indentSize_desc')}
+        </p>
+      </div>
+    );
+  };
+
+  const renderIndentForceOption = (props) => {
+    const { t, adminMarkDownContainer } = props;
+    const { isIndentSizeForced } = adminMarkDownContainer.state;
+
+    const helpIndentInComment = { __html: t('admin:markdown_setting.indent_options.disallow_indent_change_desc') };
+
+    return (
+      <div className="col">
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            type="checkbox"
+            className="custom-control-input"
+            id="isIndentSizeForced"
+            checked={isIndentSizeForced || false}
+            onChange={() => {
+              adminMarkDownContainer.setState({ isIndentSizeForced: !isIndentSizeForced });
+            }}
+          />
+          <label className="custom-control-label" htmlFor="isIndentSizeForced">
+            {t('admin:markdown_setting.indent_options.disallow_indent_change')}
+          </label>
+        </div>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpIndentInComment} />
+      </div>
+    );
+  };
+
+  const { adminMarkDownContainer } = props;
+
+  return (
+    <React.Fragment>
+      <fieldset className="form-group row row-cols-1 row-cols-md-2 mx-3">
+        {renderIndentSizeOption(props)}
+        {renderIndentForceOption(props)}
+      </fieldset>
+      <AdminUpdateButtonRow onClick={() => onClickSubmit(props)} disabled={adminMarkDownContainer.state.retrieveError != null} />
+    </React.Fragment>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]);
+
+IndentForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+};
+
+export default withTranslation()(IndentFormWrapper);

+ 8 - 0
src/client/js/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
 import LineBreakForm from './LineBreakForm';
 import LineBreakForm from './LineBreakForm';
+import IndentForm from './IndentForm';
 import PresentationForm from './PresentationForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 import XssForm from './XssForm';
 
 
@@ -21,6 +22,13 @@ class MarkDownSettingContents extends React.Component {
         </Card>
         </Card>
         <LineBreakForm />
         <LineBreakForm />
 
 
+        {/* Indent Setting */}
+        <h2 className="admin-setting-header">{t('admin:markdown_setting.indent_header')}</h2>
+        <Card className="card well my-3">
+          <CardBody className="px-0 py-2">{t('admin:markdown_setting.indent_desc') }</CardBody>
+        </Card>
+        <IndentForm />
+
         {/* Presentation Setting */}
         {/* Presentation Setting */}
         <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
         <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
         <Card className="card well my-3">
         <Card className="card well my-3">

+ 2 - 2
src/client/js/components/Navbar/PageEditorModeManager.jsx

@@ -43,7 +43,7 @@ function PageEditorModeManager(props) {
   const isAdmin = appContainer.isAdmin;
   const isAdmin = appContainer.isAdmin;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const isHackmdEnabled = appContainer.config.env.HACKMD_URI != null;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
   const showHackmdBtn = isHackmdEnabled || isAdmin;
-  const showHackmdDisabledTooltip = isAdmin && !isHackmdEnabled;
+  const showHackmdDisabledTooltip = isAdmin && !isHackmdEnabled && editorMode !== 'hackmd';
 
 
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
   const pageEditorModeButtonClickedHandler = useCallback((viewType) => {
     if (isBtnDisabled) {
     if (isBtnDisabled) {
@@ -101,7 +101,7 @@ function PageEditorModeManager(props) {
       )}
       )}
       {!isBtnDisabled && showHackmdDisabledTooltip && (
       {!isBtnDisabled && showHackmdDisabledTooltip && (
         <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager-hackmd-button" fade={false}>
         <UncontrolledTooltip placement="top" target="grw-page-editor-mode-manager-hackmd-button" fade={false}>
-          {t('HackMD editor is not available')}
+          {t('hackmd.not_set_up')}
         </UncontrolledTooltip>
         </UncontrolledTooltip>
       )}
       )}
     </>
     </>

+ 5 - 0
src/client/js/components/PageComment/CommentEditor.jsx

@@ -23,6 +23,7 @@ import CommentPreview from './CommentPreview';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 
 
+
 const navTabMapping = {
 const navTabMapping = {
   comment_editor: {
   comment_editor: {
     Icon: () => <i className="icon-settings" />,
     Icon: () => <i className="icon-settings" />,
@@ -314,6 +315,10 @@ class CommentEditor extends React.Component {
                 onUpload={this.uploadHandler}
                 onUpload={this.uploadHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
               />
               />
+              {/*
+                Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
+                See a review comment in https://github.com/weseek/growi/pull/3473
+              */}
             </TabPane>
             </TabPane>
             <TabPane tabId="comment_preview">
             <TabPane tabId="comment_preview">
               <div className="comment-form-preview">
               <div className="comment-form-preview">

+ 10 - 0
src/client/js/components/PageEditor.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
+import detectIndent from 'detect-indent';
 
 
 import { throttle, debounce } from 'throttle-debounce';
 import { throttle, debounce } from 'throttle-debounce';
 import { envUtils } from 'growi-commons';
 import { envUtils } from 'growi-commons';
@@ -59,6 +60,15 @@ class PageEditor extends React.Component {
       this.setState({ markdown: value });
       this.setState({ markdown: value });
     }));
     }));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
+
+    // Detect indent size from contents (only when users are allowed to change it)
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
+    if (!this.props.appContainer.config.isIndentSizeForced && this.state.markdown) {
+      const detectedIndent = detectIndent(this.state.markdown);
+      if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
+        this.props.editorContainer.setState({ indentSize: detectedIndent.amount });
+      }
+    }
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {

+ 1 - 1
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -843,7 +843,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             styleActiveLine: this.props.editorOptions.styleActiveLine,
             styleActiveLine: this.props.editorOptions.styleActiveLine,
             lineNumbers: this.props.lineNumbers,
             lineNumbers: this.props.lineNumbers,
             tabSize: 4,
             tabSize: 4,
-            indentUnit: 4,
+            indentUnit: this.props.indentSize,
             lineWrapping: true,
             lineWrapping: true,
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoCloseTags: true,
             autoCloseTags: true,

+ 1 - 0
src/client/js/components/PageEditor/Editor.jsx

@@ -309,6 +309,7 @@ export default class Editor extends AbstractEditor {
                       // eslint-disable-next-line arrow-body-style
                       // eslint-disable-next-line arrow-body-style
                       <CodeMirrorEditor
                       <CodeMirrorEditor
                         ref={(c) => { this.cmEditor = c }}
                         ref={(c) => { this.cmEditor = c }}
+                        indentSize={editorContainer.state.indentSize}
                         editorOptions={editorContainer.state.editorOptions}
                         editorOptions={editorContainer.state.editorOptions}
                         onPasteFiles={this.pasteFilesHandler}
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}
                         onDragEnter={this.dragEnterHandler}

+ 39 - 1
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -44,6 +44,7 @@ class OptionsSelector extends React.Component {
       emacs: 'Emacs',
       emacs: 'Emacs',
       sublime: 'Sublime Text',
       sublime: 'Sublime Text',
     };
     };
+    this.typicalIndentSizes = [2, 4];
 
 
     this.onChangeTheme = this.onChangeTheme.bind(this);
     this.onChangeTheme = this.onChangeTheme.bind(this);
     this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
     this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
@@ -51,6 +52,7 @@ class OptionsSelector extends React.Component {
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
+    this.onChangeIndentSize = this.onChangeIndentSize.bind(this);
   }
   }
 
 
   onChangeTheme(newValue) {
   onChangeTheme(newValue) {
@@ -113,6 +115,11 @@ class OptionsSelector extends React.Component {
     this.setState({ isCddMenuOpened: !this.state.isCddMenuOpened });
     this.setState({ isCddMenuOpened: !this.state.isCddMenuOpened });
   }
   }
 
 
+  onChangeIndentSize(newValue) {
+    const { editorContainer } = this.props;
+    editorContainer.setState({ indentSize: newValue });
+  }
+
   renderThemeSelector() {
   renderThemeSelector() {
     const { editorContainer } = this.props;
     const { editorContainer } = this.props;
 
 
@@ -279,11 +286,42 @@ class OptionsSelector extends React.Component {
     );
     );
   }
   }
 
 
+  renderIndentSizeSelector() {
+    const { appContainer, editorContainer } = this.props;
+    const menuItems = this.typicalIndentSizes.map((indent) => {
+      return <button key={indent} className="dropdown-item" type="button" onClick={() => this.onChangeIndentSize(indent)}>{indent}</button>;
+    });
+    return (
+      <div className="input-group flex-nowrap">
+        <div className="input-group-prepend">
+          <span className="input-group-text" id="igt-indent">Indent</span>
+        </div>
+        <div className="input-group-append dropup">
+          <button
+            type="button"
+            className="btn btn-outline-secondary dropdown-toggle"
+            data-toggle="dropdown"
+            aria-haspopup="true"
+            aria-expanded="false"
+            aria-describedby="igt-indent"
+            disabled={appContainer.config.isIndentSizeForced}
+          >
+            {editorContainer.state.indentSize}
+          </button>
+          <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
+            {menuItems}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
   render() {
   render() {
     return (
     return (
       <div className="d-flex flex-row">
       <div className="d-flex flex-row">
         <span>{this.renderThemeSelector()}</span>
         <span>{this.renderThemeSelector()}</span>
-        <span className="ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
+        <span className="d-none d-sm-block ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
+        <span className="ml-2 ml-sm-4">{this.renderIndentSizeSelector()}</span>
         <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
         <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
       </div>
       </div>
     );
     );

+ 4 - 1
src/client/js/components/Sidebar/CustomSidebar.jsx

@@ -57,7 +57,10 @@ const CustomSidebar = (props) => {
   return (
   return (
     <>
     <>
       <div className="grw-sidebar-content-header p-3 d-flex">
       <div className="grw-sidebar-content-header p-3 d-flex">
-        <h3 className="mb-0">Custom Sidebar</h3>
+        <h3 className="mb-0">
+          Custom Sidebar
+          <a className="h6 ml-2" href="/Sidebar"><i className="icon-pencil"></i></a>
+        </h3>
         <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={fetchDataAndRenderHtml}>
         <button type="button" className="btn btn-sm btn-outline-secondary ml-auto" onClick={fetchDataAndRenderHtml}>
           <i className="icon icon-reload"></i>
           <i className="icon icon-reload"></i>
         </button>
         </button>

+ 2 - 2
src/client/js/components/TableOfContents.jsx

@@ -33,8 +33,8 @@ const TableOfContents = (props) => {
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerComputedStyle = getComputedStyle(containerElem);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
     const containerPaddingTop = parseFloat(containerComputedStyle['padding-top']);
 
 
-    // get smaller bottom line of window height - .system-version height) and containerTop
-    let bottom = Math.min(window.innerHeight - 20, parentBottom);
+    // get smaller bottom line of window height - the height of ContentLinkButtons and .system-version height) and containerTop
+    let bottom = Math.min(window.innerHeight - 41 - 20, parentBottom);
 
 
     if (isUserPage) {
     if (isUserPage) {
       // raise the bottom line by the height and margin-top of UserContentLinks
       // raise the bottom line by the height and margin-top of UserContentLinks

+ 22 - 0
src/client/js/services/AdminMarkDownContainer.js

@@ -18,6 +18,8 @@ export default class AdminMarkDownContainer extends Container {
       // set dummy value tile for using suspense
       // set dummy value tile for using suspense
       isEnabledLinebreaks: this.dummyIsEnabledLinebreaks,
       isEnabledLinebreaks: this.dummyIsEnabledLinebreaks,
       isEnabledLinebreaksInComments: false,
       isEnabledLinebreaksInComments: false,
+      adminPreferredIndentSize: 4,
+      isIndentSizeForced: false,
       pageBreakSeparator: 1,
       pageBreakSeparator: 1,
       pageBreakCustomSeparator: '',
       pageBreakCustomSeparator: '',
       isEnabledXss: false,
       isEnabledXss: false,
@@ -27,6 +29,7 @@ export default class AdminMarkDownContainer extends Container {
     };
     };
 
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
     this.switchEnableXss = this.switchEnableXss.bind(this);
+    this.setAdminPreferredIndentSize = this.setAdminPreferredIndentSize.bind(this);
   }
   }
 
 
   /**
   /**
@@ -46,6 +49,8 @@ export default class AdminMarkDownContainer extends Container {
     this.setState({
     this.setState({
       isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
       isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
       isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
       isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
+      adminPreferredIndentSize: markdownParams.adminPreferredIndentSize,
+      isIndentSizeForced: markdownParams.isIndentSizeForced,
       pageBreakSeparator: markdownParams.pageBreakSeparator,
       pageBreakSeparator: markdownParams.pageBreakSeparator,
       pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
       pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
       isEnabledXss: markdownParams.isEnabledXss,
       isEnabledXss: markdownParams.isEnabledXss,
@@ -55,6 +60,10 @@ export default class AdminMarkDownContainer extends Container {
     });
     });
   }
   }
 
 
+  setAdminPreferredIndentSize(adminPreferredIndentSize) {
+    this.setState({ adminPreferredIndentSize });
+  }
+
   /**
   /**
    * Switch PageBreakSeparator
    * Switch PageBreakSeparator
    */
    */
@@ -92,6 +101,19 @@ export default class AdminMarkDownContainer extends Container {
     return response;
     return response;
   }
   }
 
 
+  /**
+   * Update
+   */
+  async updateIndentSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/indent', {
+      adminPreferredIndentSize: this.state.adminPreferredIndentSize,
+      isIndentSizeForced: this.state.isIndentSizeForced,
+    });
+
+    return response;
+  }
+
   /**
   /**
    * Update Xss Setting
    * Update Xss Setting
    */
    */

+ 1 - 7
src/client/js/services/AdminUserGroupDetailContainer.js

@@ -161,13 +161,7 @@ export default class AdminAdminUserGroupDetailContainer extends Container {
     // do not add users for ducaplicate
     // do not add users for ducaplicate
     if (res.data.userGroupRelation == null) { return }
     if (res.data.userGroupRelation == null) { return }
 
 
-    const { userGroupRelation } = res.data;
-
-    this.setState((prevState) => {
-      return {
-        userGroupRelations: [...prevState.userGroupRelations, userGroupRelation],
-      };
-    });
+    this.init();
   }
   }
 
 
   /**
   /**

+ 1 - 0
src/client/js/services/EditorContainer.js

@@ -35,6 +35,7 @@ export default class EditorContainer extends Container {
 
 
       editorOptions: {},
       editorOptions: {},
       previewOptions: {},
       previewOptions: {},
+      indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
     };
     };
 
 
     this.isSetBeforeunloadEventHandler = false;
     this.isSetBeforeunloadEventHandler = false;

+ 0 - 49
src/server/models/bookmark.js

@@ -43,18 +43,6 @@ module.exports = function(crowi) {
     return idToCountMap;
     return idToCountMap;
   };
   };
 
 
-  bookmarkSchema.statics.populatePage = async function(bookmarks) {
-    const Bookmark = this;
-    const User = crowi.model('User');
-
-    return Bookmark.populate(bookmarks, {
-      path: 'page',
-      populate: {
-        path: 'lastUpdateUser', model: 'User', select: User.USER_PUBLIC_FIELDS,
-      },
-    });
-  };
-
   // bookmark チェック用
   // bookmark チェック用
   bookmarkSchema.statics.findByPageIdAndUserId = function(pageId, userId) {
   bookmarkSchema.statics.findByPageIdAndUserId = function(pageId, userId) {
     const Bookmark = this;
     const Bookmark = this;
@@ -70,43 +58,6 @@ module.exports = function(crowi) {
     }));
     }));
   };
   };
 
 
-  /**
-   * option = {
-   *  limit: Int
-   *  offset: Int
-   *  requestUser: User
-   * }
-   */
-  bookmarkSchema.statics.findByUser = function(user, option) {
-    const Bookmark = this;
-    const requestUser = option.requestUser || null;
-
-    debug('Finding bookmark with requesting user:', requestUser);
-
-    const limit = option.limit || 50;
-    const offset = option.offset || 0;
-    const populatePage = option.populatePage || false;
-
-    return new Promise(((resolve, reject) => {
-      Bookmark
-        .find({ user: user._id })
-        .sort({ createdAt: -1 })
-        .skip(offset)
-        .limit(limit)
-        .exec((err, bookmarks) => {
-          if (err) {
-            return reject(err);
-          }
-
-          if (!populatePage) {
-            return resolve(bookmarks);
-          }
-
-          return Bookmark.populatePage(bookmarks, requestUser).then(resolve);
-        });
-    }));
-  };
-
   bookmarkSchema.statics.add = async function(page, user) {
   bookmarkSchema.statics.add = async function(page, user) {
     const Bookmark = this;
     const Bookmark = this;
 
 

+ 4 - 0
src/server/models/config.js

@@ -137,6 +137,8 @@ module.exports = function(crowi) {
       'markdown:xss:attrWhiteList': [],
       'markdown:xss:attrWhiteList': [],
       'markdown:isEnabledLinebreaks': false,
       'markdown:isEnabledLinebreaks': false,
       'markdown:isEnabledLinebreaksInComments': true,
       'markdown:isEnabledLinebreaksInComments': true,
+      'markdown:adminPreferredIndentSize': 4,
+      'markdown:isIndentSizeForced': false,
       'markdown:presentation:pageBreakSeparator': 1,
       'markdown:presentation:pageBreakSeparator': 1,
       'markdown:presentation:pageBreakCustomSeparator': undefined,
       'markdown:presentation:pageBreakCustomSeparator': undefined,
     };
     };
@@ -196,6 +198,8 @@ module.exports = function(crowi) {
       themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
       themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      adminPreferredIndentSize: crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+      isIndentSizeForced: crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),

+ 3 - 3
src/server/models/page.js

@@ -429,7 +429,7 @@ module.exports = function(crowi) {
     validateCrowi();
     validateCrowi();
 
 
     const User = crowi.model('User');
     const User = crowi.model('User');
-    return populateDataToShowRevision(this, User.USER_PUBLIC_FIELDS)
+    return populateDataToShowRevision(this, User.USER_FIELDS_EXCEPT_CONFIDENTIAL)
       .execPopulate();
       .execPopulate();
   };
   };
 
 
@@ -748,7 +748,7 @@ module.exports = function(crowi) {
     const totalCount = await builder.query.exec('count');
     const totalCount = await builder.query.exec('count');
 
 
     // find
     // find
-    builder.populateDataToList(User.USER_PUBLIC_FIELDS);
+    builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
     const pages = await builder.query.exec('find');
     const pages = await builder.query.exec('find');
 
 
     const result = {
     const result = {
@@ -791,7 +791,7 @@ module.exports = function(crowi) {
 
 
     // find
     // find
     builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
     builder.addConditionToPagenate(opt.offset, opt.limit, sortOpt);
-    builder.populateDataToList(User.USER_PUBLIC_FIELDS);
+    builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
     const pages = await builder.query.exec('find');
     const pages = await builder.query.exec('find');
 
 
     const result = {
     const result = {

+ 0 - 57
src/server/models/revision.js

@@ -27,43 +27,6 @@ module.exports = function(crowi) {
   });
   });
   revisionSchema.plugin(mongoosePaginate);
   revisionSchema.plugin(mongoosePaginate);
 
 
-  /*
-   * preparation for https://github.com/weseek/growi/issues/216
-   */
-  // // create a XSS Filter instance
-  // // TODO read options
-  // this.xss = new Xss(true);
-  // // prevent XSS when pre save
-  // revisionSchema.pre('save', function(next) {
-  //   this.body = xss.process(this.body);
-  //   next();
-  // });
-
-  revisionSchema.statics.findRevisions = function(ids) {
-    const Revision = this;
-
-
-    const User = crowi.model('User');
-
-    if (!Array.isArray(ids)) {
-      return Promise.reject(new Error('The argument was not Array.'));
-    }
-
-    return new Promise(((resolve, reject) => {
-      Revision
-        .find({ _id: { $in: ids } })
-        .sort({ createdAt: -1 })
-        .populate('author', User.USER_PUBLIC_FIELDS)
-        .exec((err, revisions) => {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(revisions);
-        });
-    }));
-  };
-
   revisionSchema.statics.findRevisionIdList = function(path) {
   revisionSchema.statics.findRevisionIdList = function(path) {
     return this.find({ path })
     return this.find({ path })
       .select('_id author createdAt hasDiffToPrev')
       .select('_id author createdAt hasDiffToPrev')
@@ -71,26 +34,6 @@ module.exports = function(crowi) {
       .exec();
       .exec();
   };
   };
 
 
-  revisionSchema.statics.findRevisionList = function(path, options) {
-    const Revision = this;
-
-
-    const User = crowi.model('User');
-
-    return new Promise(((resolve, reject) => {
-      Revision.find({ path })
-        .sort({ createdAt: -1 })
-        .populate('author', User.USER_PUBLIC_FIELDS)
-        .exec((err, data) => {
-          if (err) {
-            return reject(err);
-          }
-
-          return resolve(data);
-        });
-    }));
-  };
-
   revisionSchema.statics.updateRevisionListByPath = function(path, updateData, options) {
   revisionSchema.statics.updateRevisionListByPath = function(path, updateData, options) {
     const Revision = this;
     const Revision = this;
 
 

+ 1 - 5
src/server/models/user-group-relation.js

@@ -85,14 +85,10 @@ class UserGroupRelation {
    * @memberof UserGroupRelation
    * @memberof UserGroupRelation
    */
    */
   static findAllRelationForUserGroup(userGroup) {
   static findAllRelationForUserGroup(userGroup) {
-    const User = UserGroupRelation.crowi.model('User');
     debug('findAllRelationForUserGroup is called', userGroup);
     debug('findAllRelationForUserGroup is called', userGroup);
     return this
     return this
       .find({ relatedGroup: userGroup })
       .find({ relatedGroup: userGroup })
-      .populate({
-        path: 'relatedUser',
-        select: User.USER_PUBLIC_FIELDS,
-      })
+      .populate('relatedUser')
       .exec();
       .exec();
   }
   }
 
 

+ 2 - 2
src/server/models/user-group.js

@@ -90,7 +90,7 @@ class UserGroup {
   }
   }
 
 
   // グループの完全削除
   // グループの完全削除
-  static async removeCompletelyById(deleteGroupId, action, transferToUserGroupId) {
+  static async removeCompletelyById(deleteGroupId, action, transferToUserGroupId, user) {
     const UserGroupRelation = mongoose.model('UserGroupRelation');
     const UserGroupRelation = mongoose.model('UserGroupRelation');
 
 
     const groupToDelete = await this.findById(deleteGroupId);
     const groupToDelete = await this.findById(deleteGroupId);
@@ -101,7 +101,7 @@ class UserGroup {
 
 
     await Promise.all([
     await Promise.all([
       UserGroupRelation.removeAllByUserGroup(deletedGroup),
       UserGroupRelation.removeAllByUserGroup(deletedGroup),
-      UserGroup.crowi.pageService.handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId),
+      UserGroup.crowi.pageService.handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId, user),
     ]);
     ]);
 
 
     return deletedGroup;
     return deletedGroup;

+ 2 - 2
src/server/models/user.js

@@ -21,7 +21,7 @@ module.exports = function(crowi) {
   const STATUS_SUSPENDED = 3;
   const STATUS_SUSPENDED = 3;
   const STATUS_DELETED = 4;
   const STATUS_DELETED = 4;
   const STATUS_INVITED = 5;
   const STATUS_INVITED = 5;
-  const USER_PUBLIC_FIELDS = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
+  const USER_FIELDS_EXCEPT_CONFIDENTIAL = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
   + ' status lang createdAt lastLoginAt admin imageUrlCached';
   + ' status lang createdAt lastLoginAt admin imageUrlCached';
 
 
   const PAGE_ITEMS = 50;
   const PAGE_ITEMS = 50;
@@ -724,7 +724,7 @@ module.exports = function(crowi) {
   userSchema.statics.STATUS_SUSPENDED = STATUS_SUSPENDED;
   userSchema.statics.STATUS_SUSPENDED = STATUS_SUSPENDED;
   userSchema.statics.STATUS_DELETED = STATUS_DELETED;
   userSchema.statics.STATUS_DELETED = STATUS_DELETED;
   userSchema.statics.STATUS_INVITED = STATUS_INVITED;
   userSchema.statics.STATUS_INVITED = STATUS_INVITED;
-  userSchema.statics.USER_PUBLIC_FIELDS = USER_PUBLIC_FIELDS;
+  userSchema.statics.USER_FIELDS_EXCEPT_CONFIDENTIAL = USER_FIELDS_EXCEPT_CONFIDENTIAL;
   userSchema.statics.PAGE_ITEMS = PAGE_ITEMS;
   userSchema.statics.PAGE_ITEMS = PAGE_ITEMS;
 
 
   return mongoose.model('User', userSchema);
   return mongoose.model('User', userSchema);

+ 4 - 5
src/server/routes/apiv3/attachment.js

@@ -6,6 +6,7 @@ const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
 const { query } = require('express-validator');
 const { query } = require('express-validator');
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -69,15 +70,13 @@ module.exports = (crowi) => {
         {
         {
           limit,
           limit,
           offset,
           offset,
-          populate: {
-            path: 'creator',
-            select: User.USER_PUBLIC_FIELDS,
-          },
+          populate: 'creator',
         },
         },
       );
       );
+
       paginateResult.docs.forEach((doc) => {
       paginateResult.docs.forEach((doc) => {
         if (doc.creator != null && doc.creator instanceof User) {
         if (doc.creator != null && doc.creator instanceof User) {
-          doc.creator = doc.creator.toObject();
+          doc.creator = serializeUserSecurely(doc.creator);
         }
         }
       });
       });
 
 

+ 8 - 1
src/server/routes/apiv3/bookmarks.js

@@ -4,6 +4,7 @@ const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-
 
 
 const express = require('express');
 const express = require('express');
 const { body, query, param } = require('express-validator');
 const { body, query, param } = require('express-validator');
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 
 const router = express.Router();
 const router = express.Router();
 
 
@@ -205,13 +206,19 @@ module.exports = (crowi) => {
             populate: {
             populate: {
               path: 'lastUpdateUser',
               path: 'lastUpdateUser',
               model: 'User',
               model: 'User',
-              select: User.USER_PUBLIC_FIELDS,
             },
             },
           },
           },
           page,
           page,
           limit,
           limit,
         },
         },
       );
       );
+
+      paginationResult.docs.forEach((doc) => {
+        if (doc.page.lastUpdateUser != null && doc.page.lastUpdateUser instanceof User) {
+          doc.page.lastUpdateUser = serializeUserSecurely(doc.page.lastUpdateUser);
+        }
+      });
+
       return res.apiv3({ paginationResult });
       return res.apiv3({ paginationResult });
     }
     }
     catch (err) {
     catch (err) {

+ 29 - 0
src/server/routes/apiv3/markdown-setting.js

@@ -15,6 +15,10 @@ const validator = {
     body('isEnabledLinebreaks').isBoolean(),
     body('isEnabledLinebreaks').isBoolean(),
     body('isEnabledLinebreaksInComments').isBoolean(),
     body('isEnabledLinebreaksInComments').isBoolean(),
   ],
   ],
+  indent: [
+    body('adminPreferredIndentSize').isIn([2, 4]),
+    body('isIndentSizeForced').isBoolean(),
+  ],
   presentationSetting: [
   presentationSetting: [
     body('pageBreakSeparator').isInt().not().isEmpty(),
     body('pageBreakSeparator').isInt().not().isEmpty(),
   ],
   ],
@@ -111,6 +115,8 @@ module.exports = (crowi) => {
     const markdownParams = {
     const markdownParams = {
       isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
       isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      adminPreferredIndentSize: await crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+      isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
       isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
@@ -168,6 +174,29 @@ module.exports = (crowi) => {
 
 
   });
   });
 
 
+  router.put('/indent', loginRequiredStrictly, adminRequired, csrf, validator.indent, apiV3FormValidator, async(req, res) => {
+
+    const requestIndentParams = {
+      'markdown:adminPreferredIndentSize': req.body.adminPreferredIndentSize,
+      'markdown:isIndentSizeForced': req.body.isIndentSizeForced,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', requestIndentParams);
+      const indentParams = {
+        adminPreferredIndentSize: await crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+        isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+      };
+      return res.apiv3({ indentParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating indent';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-indent-failed'));
+    }
+
+  });
+
   /**
   /**
    * @swagger
    * @swagger
    *
    *

+ 15 - 0
src/server/routes/apiv3/pages.js

@@ -112,6 +112,7 @@ module.exports = (crowi) => {
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
 
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
+  const User = crowi.model('User');
   const PageTagRelation = crowi.model('PageTagRelation');
   const PageTagRelation = crowi.model('PageTagRelation');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
 
 
@@ -120,6 +121,7 @@ module.exports = (crowi) => {
 
 
   const { serializePageSecurely } = require('../../models/serializers/page-serializer');
   const { serializePageSecurely } = require('../../models/serializers/page-serializer');
   const { serializeRevisionSecurely } = require('../../models/serializers/revision-serializer');
   const { serializeRevisionSecurely } = require('../../models/serializers/revision-serializer');
+  const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 
   const validator = {
   const validator = {
     createPage: [
     createPage: [
@@ -299,6 +301,12 @@ module.exports = (crowi) => {
         result.pages.pop();
         result.pages.pop();
       }
       }
 
 
+      result.pages.forEach((page) => {
+        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+        }
+      });
+
       return res.apiv3(result);
       return res.apiv3(result);
     }
     }
     catch (err) {
     catch (err) {
@@ -470,6 +478,13 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);
+
+      result.pages.forEach((page) => {
+        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+        }
+      });
+
       return res.apiv3(result);
       return res.apiv3(result);
     }
     }
     catch (err) {
     catch (err) {

+ 14 - 5
src/server/routes/apiv3/revisions.js

@@ -5,6 +5,7 @@ const logger = loggerFactory('growi:routes:apiv3:pages');
 const express = require('express');
 const express = require('express');
 
 
 const { query, param } = require('express-validator');
 const { query, param } = require('express-validator');
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const router = express.Router();
 const router = express.Router();
@@ -128,13 +129,16 @@ module.exports = (crowi) => {
           page: selectedPage,
           page: selectedPage,
           limit,
           limit,
           sort: { createdAt: -1 },
           sort: { createdAt: -1 },
-          populate: {
-            path: 'author',
-            select: User.USER_PUBLIC_FIELDS,
-          },
+          populate: 'author',
         },
         },
       );
       );
 
 
+      paginateResult.docs.forEach((doc) => {
+        if (doc.author != null && doc.author instanceof User) {
+          doc.author = serializeUserSecurely(doc.author);
+        }
+      });
+
       return res.apiv3(paginateResult);
       return res.apiv3(paginateResult);
     }
     }
     catch (err) {
     catch (err) {
@@ -181,7 +185,12 @@ module.exports = (crowi) => {
     }
     }
 
 
     try {
     try {
-      const revision = await Revision.findById(revisionId).populate('author', User.USER_PUBLIC_FIELDS);
+      const revision = await Revision.findById(revisionId).populate('author');
+
+      if (revision.author != null && revision.author instanceof User) {
+        revision.author = serializeUserSecurely(revision.author);
+      }
+
       return res.apiv3({ revision });
       return res.apiv3({ revision });
     }
     }
     catch (err) {
     catch (err) {

+ 2 - 1
src/server/routes/apiv3/user-group-relation.js

@@ -4,6 +4,7 @@ const logger = loggerFactory('growi:routes:apiv3:user-group-relation'); // eslin
 
 
 const express = require('express');
 const express = require('express');
 
 
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const router = express.Router();
 const router = express.Router();
@@ -52,7 +53,7 @@ module.exports = (crowi) => {
       await Promise.all(userGroups.map(async(userGroup) => {
       await Promise.all(userGroups.map(async(userGroup) => {
         const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);
         const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);
         userGroupRelationsObj[userGroup._id] = userGroupRelations.map((userGroupRelation) => {
         userGroupRelationsObj[userGroup._id] = userGroupRelations.map((userGroupRelation) => {
-          return userGroupRelation.relatedUser;
+          return serializeUserSecurely(userGroupRelation.relatedUser);
         });
         });
       }));
       }));
 
 

+ 23 - 17
src/server/routes/apiv3/user-group.js

@@ -13,6 +13,7 @@ const mongoose = require('mongoose');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');
 const { toPagingLimit, toPagingOffset } = require('../../util/express-validator/sanitizer');
 
 
 const validator = {};
 const validator = {};
@@ -174,7 +175,7 @@ module.exports = (crowi) => {
     const { actionName, transferToUserGroupId } = req.query;
     const { actionName, transferToUserGroupId } = req.query;
 
 
     try {
     try {
-      const userGroup = await UserGroup.removeCompletelyById(deleteGroupId, actionName, transferToUserGroupId);
+      const userGroup = await UserGroup.removeCompletelyById(deleteGroupId, actionName, transferToUserGroupId, req.user);
 
 
       return res.apiv3({ userGroup });
       return res.apiv3({ userGroup });
     }
     }
@@ -288,7 +289,7 @@ module.exports = (crowi) => {
       const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);
       const userGroupRelations = await UserGroupRelation.findAllRelationForUserGroup(userGroup);
 
 
       const users = userGroupRelations.map((userGroupRelation) => {
       const users = userGroupRelations.map((userGroupRelation) => {
-        return userGroupRelation.relatedUser;
+        return serializeUserSecurely(userGroupRelation.relatedUser);
       });
       });
 
 
       return res.apiv3({ users });
       return res.apiv3({ users });
@@ -344,7 +345,14 @@ module.exports = (crowi) => {
       const userGroup = await UserGroup.findById(id);
       const userGroup = await UserGroup.findById(id);
       const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup, queryOptions);
       const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup, queryOptions);
 
 
-      return res.apiv3({ users });
+      // return email only this api
+      const serializedUsers = users.map((user) => {
+        const { email } = user;
+        const serializedUser = serializeUserSecurely(user);
+        serializedUser.email = email;
+        return serializedUser;
+      });
+      return res.apiv3({ users: serializedUsers });
     }
     }
     catch (err) {
     catch (err) {
       const msg = `Error occurred in fetching unrelated users for group: ${id}`;
       const msg = `Error occurred in fetching unrelated users for group: ${id}`;
@@ -411,9 +419,9 @@ module.exports = (crowi) => {
       }
       }
 
 
       const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
       const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
-      await userGroupRelation.populate('relatedUser', User.USER_PUBLIC_FIELDS).execPopulate();
+      const serializedUser = serializeUserSecurely(user);
 
 
-      return res.apiv3({ user, userGroup, userGroupRelation });
+      return res.apiv3({ user: serializedUser, userGroup, userGroupRelation });
     }
     }
     catch (err) {
     catch (err) {
       const msg = `Error occurred in adding the user "${username}" to group "${id}"`;
       const msg = `Error occurred in adding the user "${username}" to group "${id}"`;
@@ -471,14 +479,10 @@ module.exports = (crowi) => {
         User.findUserByUsername(username),
         User.findUserByUsername(username),
       ]);
       ]);
 
 
-      const userGroupRelation = await UserGroupRelation.findOne({ relatedUser: new ObjectId(user._id), relatedGroup: new ObjectId(userGroup._id) });
-      if (userGroupRelation == null) {
-        throw new Error(`Group "${id}" does not exist or user "${username}" does not belong to group "${id}"`);
-      }
-
-      await userGroupRelation.remove();
+      const userGroupRelation = await UserGroupRelation.findOneAndDelete({ relatedUser: new ObjectId(user._id), relatedGroup: new ObjectId(userGroup._id) });
+      const serializedUser = serializeUserSecurely(user);
 
 
-      return res.apiv3({ user, userGroup, userGroupRelation });
+      return res.apiv3({ user: serializedUser, userGroup, userGroupRelation });
     }
     }
     catch (err) {
     catch (err) {
       const msg = `Error occurred in removing the user "${username}" from group "${id}"`;
       const msg = `Error occurred in removing the user "${username}" from group "${id}"`;
@@ -584,16 +588,18 @@ module.exports = (crowi) => {
       }, {
       }, {
         offset,
         offset,
         limit,
         limit,
-        populate: {
-          path: 'lastUpdateUser',
-          select: User.USER_PUBLIC_FIELDS,
-        },
+        populate: 'lastUpdateUser',
       });
       });
 
 
       const current = offset / limit + 1;
       const current = offset / limit + 1;
 
 
+      const pages = docs.map((doc) => {
+        doc.lastUpdateUser = serializeUserSecurely(doc.lastUpdateUser);
+        return doc;
+      });
+
       // TODO: create a common moudule for paginated response
       // TODO: create a common moudule for paginated response
-      return res.apiv3({ total: totalDocs, current, pages: docs });
+      return res.apiv3({ total: totalDocs, current, pages });
     }
     }
     catch (err) {
     catch (err) {
       const msg = `Error occurred in fetching pages for group: ${id}`;
       const msg = `Error occurred in fetching pages for group: ${id}`;

+ 8 - 8
src/server/routes/comment.js

@@ -4,6 +4,8 @@
  *    name: Comments
  *    name: Comments
  */
  */
 
 
+const { serializeUserSecurely } = require('../models/serializers/user-serializer');
+
 /**
 /**
  * @swagger
  * @swagger
  *
  *
@@ -130,9 +132,12 @@ module.exports = function(crowi, app) {
       return res.json(ApiResponse.error(err));
       return res.json(ApiResponse.error(err));
     }
     }
 
 
-    const comments = await fetcher.populate(
-      { path: 'creator', select: User.USER_PUBLIC_FIELDS },
-    );
+    const comments = await fetcher.populate('creator');
+    comments.forEach((comment) => {
+      if (comment.creator != null && comment.creator instanceof User) {
+        comment.creator = serializeUserSecurely(comment.creator);
+      }
+    });
 
 
     res.json(ApiResponse.success({ comments }));
     res.json(ApiResponse.success({ comments }));
   };
   };
@@ -234,11 +239,6 @@ module.exports = function(crowi, app) {
     let createdComment;
     let createdComment;
     try {
     try {
       createdComment = await Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown, replyTo);
       createdComment = await Comment.create(pageId, req.user._id, revisionId, comment, position, isMarkdown, replyTo);
-
-      await Comment.populate(createdComment, [
-        { path: 'creator', model: 'User', select: User.USER_PUBLIC_FIELDS },
-      ]);
-
     }
     }
     catch (err) {
     catch (err) {
       logger.error(err);
       logger.error(err);

+ 0 - 1
src/server/routes/index.js

@@ -133,7 +133,6 @@ module.exports = function(crowi, app) {
 
 
   app.get('/_api/check_username'           , user.api.checkUsername);
   app.get('/_api/check_username'           , user.api.checkUsername);
   app.get('/_api/me/user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.userGroupRelations);
   app.get('/_api/me/user-group-relations'  , accessTokenParser , loginRequiredStrictly , me.api.userGroupRelations);
-  app.get('/_api/user/bookmarks'           , loginRequired , user.api.bookmarks);
 
 
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   // HTTP RPC Styled API (に徐々に移行していいこうと思う)
   app.get('/_api/users.list'          , accessTokenParser , loginRequired , user.api.list);
   app.get('/_api/users.list'          , accessTokenParser , loginRequired , user.api.list);

+ 10 - 5
src/server/routes/page.js

@@ -1,5 +1,6 @@
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
+const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 
 
 /**
 /**
  * @swagger
  * @swagger
@@ -134,7 +135,6 @@ module.exports = function(crowi, app) {
 
 
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
   const User = crowi.model('User');
   const User = crowi.model('User');
-  const Bookmark = crowi.model('Bookmark');
   const PageTagRelation = crowi.model('PageTagRelation');
   const PageTagRelation = crowi.model('PageTagRelation');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const GlobalNotificationSetting = crowi.model('GlobalNotificationSetting');
   const ShareLink = crowi.model('ShareLink');
   const ShareLink = crowi.model('ShareLink');
@@ -222,12 +222,11 @@ module.exports = function(crowi, app) {
     renderVars.revision = page.revision;
     renderVars.revision = page.revision;
   }
   }
 
 
-  async function addRenderVarsForUserPage(renderVars, page, requestUser) {
+  async function addRenderVarsForUserPage(renderVars, page) {
     const userData = await User.findUserByUsername(User.getUsernameByPath(page.path));
     const userData = await User.findUserByUsername(User.getUsernameByPath(page.path));
 
 
     if (userData != null) {
     if (userData != null) {
-      renderVars.pageUser = userData.toObject();
-      renderVars.bookmarkList = await Bookmark.findByUser(userData, { limit: 10, populatePage: true, requestUser });
+      renderVars.pageUser = serializeUserSecurely(userData);
     }
     }
   }
   }
 
 
@@ -371,7 +370,7 @@ module.exports = function(crowi, app) {
     if (isUserPage(page.path)) {
     if (isUserPage(page.path)) {
       // change template
       // change template
       view = 'layout-growi/user_page';
       view = 'layout-growi/user_page';
-      await addRenderVarsForUserPage(renderVars, page, req.user);
+      await addRenderVarsForUserPage(renderVars, page);
     }
     }
 
 
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
@@ -654,6 +653,12 @@ module.exports = function(crowi, app) {
         result.pages.pop();
         result.pages.pop();
       }
       }
 
 
+      result.pages.forEach((page) => {
+        if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+          page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+        }
+      });
+
       return res.json(ApiResponse.success(result));
       return res.json(ApiResponse.success(result));
     }
     }
     catch (err) {
     catch (err) {

+ 6 - 0
src/server/routes/search.js

@@ -1,3 +1,5 @@
+const { serializeUserSecurely } = require('../models/serializers/user-serializer');
+
 /**
 /**
  * @swagger
  * @swagger
  *
  *
@@ -27,6 +29,7 @@
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   // var debug = require('debug')('growi:routes:search')
   // var debug = require('debug')('growi:routes:search')
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
+  const User = crowi.model('User');
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
   const ApiPaginate = require('../util/apiPaginate');
   const ApiPaginate = require('../util/apiPaginate');
 
 
@@ -159,6 +162,9 @@ module.exports = function(crowi, app) {
       result.totalCount = findResult.totalCount;
       result.totalCount = findResult.totalCount;
       result.data = findResult.pages
       result.data = findResult.pages
         .map((page) => {
         .map((page) => {
+          if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
+            page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+          }
           page.bookmarkCount = (page._source && page._source.bookmark_count) || 0;
           page.bookmarkCount = (page._source && page._source.bookmark_count) || 0;
           return page;
           return page;
         })
         })

+ 0 - 11
src/server/routes/user.js

@@ -47,7 +47,6 @@
 
 
 module.exports = function(crowi, app) {
 module.exports = function(crowi, app) {
   const User = crowi.model('User');
   const User = crowi.model('User');
-  const Bookmark = crowi.model('Bookmark');
   const ApiResponse = require('../util/apiResponse');
   const ApiResponse = require('../util/apiResponse');
 
 
   const actions = {};
   const actions = {};
@@ -57,16 +56,6 @@ module.exports = function(crowi, app) {
 
 
   actions.api = api;
   actions.api = api;
 
 
-  api.bookmarks = function(req, res) {
-    const options = {
-      skip: req.query.offset || 0,
-      limit: req.query.limit || 50,
-    };
-    Bookmark.findByUser(req.user, options, (err, bookmarks) => {
-      res.json(bookmarks);
-    });
-  };
-
   api.checkUsername = function(req, res) {
   api.checkUsername = function(req, res) {
     const username = req.query.username;
     const username = req.query.username;
 
 

+ 2 - 2
src/server/service/page.js

@@ -721,7 +721,7 @@ class PageService {
   }
   }
 
 
 
 
-  async handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId) {
+  async handlePrivatePagesForDeletedGroup(deletedGroup, action, transferToUserGroupId, user) {
     const Page = this.crowi.model('Page');
     const Page = this.crowi.model('Page');
     const pages = await Page.find({ grantedGroup: deletedGroup });
     const pages = await Page.find({ grantedGroup: deletedGroup });
 
 
@@ -732,7 +732,7 @@ class PageService {
         }));
         }));
         break;
         break;
       case 'delete':
       case 'delete':
-        return this.deleteMultiplePagesCompletely(pages);
+        return this.deleteMultipleCompletely(pages, user);
       case 'transfer':
       case 'transfer':
         await Promise.all(pages.map((page) => {
         await Promise.all(pages.map((page) => {
           return Page.transferPageToGroup(page, transferToUserGroupId);
           return Page.transferPageToGroup(page, transferToUserGroupId);

+ 5 - 0
yarn.lock

@@ -4938,6 +4938,11 @@ detect-indent@^5.0.0:
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
   integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
   integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
 
 
+detect-indent@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd"
+  integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==
+
 detect-libc@^1.0.2:
 detect-libc@^1.0.2:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"