Преглед изворни кода

Merge branch 'master' into imprv/master-gw4312

itizawa пре 5 година
родитељ
комит
372175c798
77 измењених фајлова са 379 додато и 469 уклоњено
  1. 5 0
      CHANGES.md
  2. 2 2
      bin/github-actions/update-readme.sh
  3. 5 5
      docker/README.md
  4. 3 1
      resource/locales/en_US/translation.json
  5. 3 1
      resource/locales/ja_JP/translation.json
  6. 4 2
      resource/locales/zh_CN/translation.json
  7. 2 0
      src/client/js/app.jsx
  8. 10 2
      src/client/js/components/Hotkeys/Subscribers/EditPage.jsx
  9. 5 0
      src/client/js/components/Icons/LooockIcon.jsx
  10. 5 0
      src/client/js/components/Icons/PaperPlaneIcon.jsx
  11. 5 0
      src/client/js/components/Icons/ShareAltIcon.jsx
  12. 5 0
      src/client/js/components/Icons/UserIcon.jsx
  13. 5 0
      src/client/js/components/InstallerForm.jsx
  14. 1 1
      src/client/js/components/LikeButton.jsx
  15. 36 42
      src/client/js/components/Me/PersonalSettings.jsx
  16. 4 4
      src/client/js/components/MyDraftList/Draft.jsx
  17. 2 3
      src/client/js/components/MyDraftList/MyDraftList.jsx
  18. 14 3
      src/client/js/components/Navbar/AuthorInfo.jsx
  19. 1 2
      src/client/js/components/Navbar/GrowiNavbar.jsx
  20. 11 7
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  21. 1 1
      src/client/js/components/Page.jsx
  22. 1 1
      src/client/js/components/Page/ShareLinkAlert.jsx
  23. 6 4
      src/client/js/components/PageAccessoriesModalControl.jsx
  24. 1 1
      src/client/js/components/PageComments.jsx
  25. 39 0
      src/client/js/components/PageContentFooter.jsx
  26. 21 9
      src/client/js/components/TableOfContents.jsx
  27. 1 1
      src/client/js/components/User/UserInfo.jsx
  28. 0 24
      src/client/js/legacy/crowi.js
  29. 16 18
      src/client/js/services/PageContainer.js
  30. 7 3
      src/client/js/services/TagContainer.js
  31. 0 41
      src/client/styles/scss/_attachments.scss
  32. 4 10
      src/client/styles/scss/_layout.scss
  33. 1 2
      src/client/styles/scss/_page-accessories-control.scss
  34. 6 0
      src/client/styles/scss/_page-content-footer.scss
  35. 1 26
      src/client/styles/scss/_page.scss
  36. 0 10
      src/client/styles/scss/_page_list.scss
  37. 1 1
      src/client/styles/scss/_subnav.scss
  38. 10 0
      src/client/styles/scss/_user.scss
  39. 0 6
      src/client/styles/scss/_user_growi.scss
  40. 1 1
      src/client/styles/scss/atoms/_buttons.scss
  41. 1 1
      src/client/styles/scss/style-app.scss
  42. 2 15
      src/client/styles/scss/theme/_apply-colors-dark.scss
  43. 2 15
      src/client/styles/scss/theme/_apply-colors-light.scss
  44. 3 3
      src/client/styles/scss/theme/_apply-colors.scss
  45. 1 1
      src/client/styles/scss/theme/kibela.scss
  46. 4 0
      src/client/styles/scss/theme/spring.scss
  47. 2 2
      src/server/middlewares/login-required.js
  48. 43 36
      src/server/routes/apiv3/bookmarks.js
  49. 0 3
      src/server/views/admin/app.html
  50. 0 2
      src/server/views/admin/customize.html
  51. 0 3
      src/server/views/admin/export.html
  52. 0 5
      src/server/views/admin/external-accounts.html
  53. 0 3
      src/server/views/admin/global-notification-detail.html
  54. 0 3
      src/server/views/admin/importer.html
  55. 0 3
      src/server/views/admin/index.html
  56. 0 10
      src/server/views/admin/markdown.html
  57. 0 3
      src/server/views/admin/notification.html
  58. 0 3
      src/server/views/admin/search.html
  59. 0 3
      src/server/views/admin/security.html
  60. 0 3
      src/server/views/admin/user-group-detail.html
  61. 0 5
      src/server/views/admin/user-groups.html
  62. 0 5
      src/server/views/admin/users.html
  63. 25 19
      src/server/views/layout-growi/base/layout.html
  64. 1 5
      src/server/views/layout-growi/forbidden.html
  65. 1 5
      src/server/views/layout-growi/not_creatable.html
  66. 1 5
      src/server/views/layout-growi/not_found.html
  67. 2 7
      src/server/views/layout-growi/page.html
  68. 3 3
      src/server/views/layout-growi/page_list.html
  69. 5 13
      src/server/views/layout-growi/user_page.html
  70. 1 4
      src/server/views/layout/admin.html
  71. 15 12
      src/server/views/me/drafts.html
  72. 15 13
      src/server/views/me/index.html
  73. 0 3
      src/server/views/search.html
  74. 0 3
      src/server/views/tags.html
  75. 0 10
      src/server/views/widget/page_attachments.html
  76. 5 4
      src/server/views/widget/page_content.html
  77. 2 2
      src/test/middlewares/login-required.test.js

+ 5 - 0
CHANGES.md

@@ -15,6 +15,11 @@
     * migrate-mongo
     * mongoose
 
+## v4.1.10
+
+* Fix: Make listing users API secure
+* Fix: Error message when the server denies guest user connecting with socket.io
+
 ## v4.1.9
 
 * Feature: Environment variables to set max connection size to deliver push messages to all clients

+ 2 - 2
bin/github-actions/update-readme.sh

@@ -2,5 +2,5 @@
 
 cd docker
 
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.1\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
-sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.1-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.2\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}\2\3${RELEASE_VERSION}\4/" README.md
+sed -i -e "s/^\([*] \[\`\)[^\`]\+\(\`, \`4\.2-nocdn\`, .\+\]\)\(.\+\/blob\/v\).\+\(\/docker\/Dockerfile.\+\)$/\1${RELEASE_VERSION}-nocdn\2\3${RELEASE_VERSION}\4/" README.md

+ 5 - 5
docker/README.md

@@ -10,10 +10,10 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`4.1.0`, `4.1`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.0/docker/Dockerfile)
-* [`4.1.0-nocdn`, `4.1-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.0/docker/Dockerfile)
-* [`4.0.11`, `4.0`(Dockerfile)](https://github.com/weseek/growi/blob/v4.0.11/docker/Dockerfile)
-* [`4.0.11-nocdn`, `4.0-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.0.11/docker/Dockerfile)
+* [`4.2.0`, `4.2`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
+* [`4.2.0-nocdn`, `4.2-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.2.0/docker/Dockerfile)
+* [`4.1.10`, `4.1` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.10/docker/Dockerfile)
+* [`4.1.10-nocdn`, `4.1-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.1.10/docker/Dockerfile)
 * [`3.8.0`, `3.8`, `3` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 * [`3.8.0-nocdn`, `3.8-nocdn`, `3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v3.8.0/docker/Dockerfile)
 
@@ -39,7 +39,7 @@ The GROWI official docker image for production use which concludes several offic
 Requirements
 -------------
 
-* MongoDB (>= 3.6)
+* MongoDB (>= 4.4)
 
 ### Optional Dependencies
 

+ 3 - 1
resource/locales/en_US/translation.json

@@ -99,7 +99,6 @@
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New page",
   "Create under": "Create page under below:",
-  "Table of Contents": "Table of Contents",
   "Wiki Management Home Page": "Wiki Management Home Page",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
@@ -295,6 +294,9 @@
       "no_deadline":"This page has no expiration date"
     }
   },
+  "page_table_of_contents": {
+    "empty": "Table of Contents is empty"
+  },
   "page_edit": {
     "Show active line": "Show active line",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",

+ 3 - 1
resource/locales/ja_JP/translation.json

@@ -100,7 +100,6 @@
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
-  "Table of Contents": "目次",
   "Wiki Management Home Page": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "Site URL settings": "サイトURL設定",
@@ -297,6 +296,9 @@
       "no_deadline": "このページに有効期限は設定されていません。"
     }
   },
+  "page_table_of_contents": {
+    "empty": "目次は空です"
+  },
   "page_edit": {
     "Show active line": "アクティブ行をハイライト",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",

+ 4 - 2
resource/locales/zh_CN/translation.json

@@ -108,7 +108,6 @@
 	"Input page name (optional)": "Input page name (optional)",
 	"New Page": "新页面",
 	"Create under": "Create page under below:",
-	"Table of Contents": "Table of Contents",
 	"Wiki Management Home Page": "Wiki管理首页",
 	"App Settings": "系统设置",
 	"Site URL settings": "主页URL设置",
@@ -282,7 +281,10 @@
 		"notice": {
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 		}
-	},
+  },
+  "page_table_of_contents": {
+    "empty": "目录为空"
+  },
   "page_comment": {
     "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
   },

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

@@ -12,6 +12,7 @@ import DisplaySwitcher from './components/Page/DisplaySwitcher';
 import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
 import Page from './components/Page';
 import PageComments from './components/PageComments';
+import PageContentFooter from './components/PageContentFooter';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageManagement from './components/Page/PageManagement';
@@ -111,6 +112,7 @@ if (pageContainer.state.pageId != null) {
     'page-accessories': <PageAccessories />,
     'revision-toc': <TableOfContents />,
     'liker-list': <LikerList />,
+    'page-content-footer': <PageContentFooter />,
 
     'recent-created-icon': <RecentlyCreatedIcon />,
     'user-bookmark-icon': <BookmarkIcon />,

+ 10 - 2
src/client/js/components/Hotkeys/Subscribers/EditPage.jsx

@@ -1,6 +1,9 @@
 import React, { useEffect } from 'react';
 import PropTypes from 'prop-types';
 
+import NavigationContainer from '../../../services/NavigationContainer';
+import { withUnstatedContainers } from '../../UnstatedUtils';
+
 const EditPage = (props) => {
 
   // setup effect
@@ -10,6 +13,8 @@ const EditPage = (props) => {
       return;
     }
 
+    props.navigationContainer.setEditorMode('edit');
+
     // remove this
     props.onDeleteRender(this);
   }, [props]);
@@ -18,11 +23,14 @@ const EditPage = (props) => {
 };
 
 EditPage.propTypes = {
+  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   onDeleteRender: PropTypes.func.isRequired,
 };
 
-EditPage.getHotkeyStrokes = () => {
+const EditPageWrapper = withUnstatedContainers(EditPage, [NavigationContainer]);
+
+EditPageWrapper.getHotkeyStrokes = () => {
   return [['e']];
 };
 
-export default EditPage;
+export default EditPageWrapper;

+ 5 - 0
src/client/js/components/Icons/LooockIcon.jsx

@@ -0,0 +1,5 @@
+import React from 'react';
+
+const LockIcon = () => <i className="icon-fw icon-lock"></i>;
+
+export default LockIcon;

+ 5 - 0
src/client/js/components/Icons/PaperPlaneIcon.jsx

@@ -0,0 +1,5 @@
+import React from 'react';
+
+const PaperPlaneIcon = () => <i className="icon-fw icon-paper-plane"></i>;
+
+export default PaperPlaneIcon;

+ 5 - 0
src/client/js/components/Icons/ShareAltIcon.jsx

@@ -0,0 +1,5 @@
+import React from 'react';
+
+const ShareAltIcon = () => <i className="icon-fw icon-share-alt"></i>;
+
+export default ShareAltIcon;

+ 5 - 0
src/client/js/components/Icons/UserIcon.jsx

@@ -0,0 +1,5 @@
+import React from 'react';
+
+const UserIcon = () => <i className="icon-fw icon-user"></i>;
+
+export default UserIcon;

+ 5 - 0
src/client/js/components/InstallerForm.jsx

@@ -72,6 +72,11 @@ class InstallerForm extends React.Component {
                     {this.state.selectedLang.displayName}
                   </span>
                 </button>
+                <input
+                  type="hidden"
+                  value={this.state.selectedLang.id}
+                  name="registerForm[app:globalLang]"
+                />
                 <div className="dropdown-menu" aria-labelledby="dropdownLanguage">
                   {
                   localeMetadatas.map(meta => (

+ 1 - 1
src/client/js/components/LikeButton.jsx

@@ -39,7 +39,7 @@ class LikeButton extends React.Component {
       <button
         type="button"
         onClick={this.handleClick}
-        className={`btn btn-like border-0 d-edit-none
+        className={`btn btn-like border-0
         ${pageContainer.state.isLiked ? 'active' : ''}`}
       >
         <i className="icon-like mr-3"></i>

+ 36 - 42
src/client/js/components/Me/PersonalSettings.jsx

@@ -1,59 +1,53 @@
 
-import React, { Fragment } from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
-
+import CustomNavigation from '../CustomNavigation';
 import UserSettings from './UserSettings';
 import PasswordSettings from './PasswordSettings';
 import ExternalAccountLinkedMe from './ExternalAccountLinkedMe';
 import ApiSettings from './ApiSettings';
 
+import UserIcon from '../Icons/UserIcon';
+import ShareAltIcon from '../Icons/ShareAltIcon';
+import LockIcon from '../Icons/LooockIcon';
+import PaperPlaneIcon from '../Icons/PaperPlaneIcon';
+
 class PersonalSettings extends React.Component {
 
   render() {
     const { t } = this.props;
 
+    const navTabMapping = {
+      user_infomation: {
+        Icon: UserIcon,
+        Content: UserSettings,
+        i18n: t('User Information'),
+        index: 0,
+      },
+      external_accounts: {
+        Icon: ShareAltIcon,
+        Content: ExternalAccountLinkedMe,
+        i18n: t('admin:user_management.external_accounts'),
+        index: 1,
+      },
+      password_settings: {
+        Icon: LockIcon,
+        Content: PasswordSettings,
+        i18n: t('Password Settings'),
+        index: 2,
+      },
+      api_settings: {
+        Icon: PaperPlaneIcon,
+        Content: ApiSettings,
+        i18n: t('API Settings'),
+        index: 3,
+      },
+    };
+
+
     return (
-      <Fragment>
-        <div className="personal-settings">
-          <ul className="nav nav-tabs" role="tablist">
-            <li className="nav-item">
-              <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab">
-                <i className="icon-fw icon-user"></i>{ t('User Information') }
-              </a>
-            </li>
-            <li className="nav-item">
-              <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
-                <i className="icon-fw icon-share-alt"></i>{ t('admin:user_management.external_accounts') }
-              </a>
-            </li>
-            <li className="nav-item">
-              <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab">
-                <i className="icon-fw icon-lock"></i>{ t('Password Settings') }
-              </a>
-            </li>
-            <li className="nav-item">
-              <a className="nav-link" href="#apiToken" data-toggle="tab" role="tab">
-                <i className="icon-fw icon-paper-plane"></i>{ t('API Settings') }
-              </a>
-            </li>
-          </ul>
-          <div className="tab-content p-t-10">
-            <div id="user-settings" className="tab-pane active" role="tabpanel">
-              <UserSettings />
-            </div>
-            <div id="external-accounts" className="tab-pane" role="tabpanel">
-              <ExternalAccountLinkedMe />
-            </div>
-            <div id="password-settings" className="tab-pane" role="tabpanel">
-              <PasswordSettings />
-            </div>
-            <div id="apiToken" className="tab-pane" role="tabpanel">
-              <ApiSettings />
-            </div>
-          </div>
-        </div>
-      </Fragment>
+      <CustomNavigation navTabMapping={navTabMapping} />
     );
   }
 

+ 4 - 4
src/client/js/components/MyDraftList/Draft.jsx

@@ -105,10 +105,9 @@ class Draft extends React.Component {
   }
 
   renderControls() {
-    const { t, path } = this.props;
+    const { t, path, index } = this.props;
 
-    const encodedPath = path.replace(/\//g, '-');
-    const tooltipTargetId = `draft-copied-tooltip_${encodedPath}`;
+    const tooltipTargetId = `draft-copied-tooltip_${index}`;
 
     return (
       <div className="icon-container">
@@ -116,7 +115,7 @@ class Draft extends React.Component {
           ? null
           : (
             <a
-              href={`${this.props.path}#edit`}
+              href={`${path}#edit`}
               target="_blank"
               rel="noopener noreferrer"
               data-toggle="tooltip"
@@ -203,6 +202,7 @@ Draft.propTypes = {
   t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
+  index: PropTypes.number.isRequired,
   path: PropTypes.string.isRequired,
   markdown: PropTypes.string.isRequired,
   isExist: PropTypes.bool.isRequired,

+ 2 - 3
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -91,9 +91,10 @@ class MyDraftList extends React.Component {
    *
    */
   generateDraftList(drafts) {
-    return drafts.map((draft) => {
+    return drafts.map((draft, index) => {
       return (
         <Draft
+          index={index}
           key={draft.path}
           path={draft.path}
           markdown={draft.markdown}
@@ -135,8 +136,6 @@ class MyDraftList extends React.Component {
 
     return (
       <div className="page-list-container-create ">
-        <h1>My Drafts</h1>
-        <hr />
         { totalCount === 0
           && <span className="mt-2">No drafts yet.</span>
         }

+ 14 - 3
src/client/js/components/Navbar/AuthorInfo.jsx

@@ -6,22 +6,31 @@ import { userPageRoot } from '@commons/util/path-utils';
 import UserPicture from '../User/UserPicture';
 
 const AuthorInfo = (props) => {
-  const { mode, user, date } = props;
+  const {
+    mode, user, date, locate,
+  } = props;
 
-  const infoLabel = mode === 'create'
+  const infoLabelForSubNav = mode === 'create'
     ? 'Created by'
     : 'Updated by';
+  const infoLabelForFooter = mode === 'create'
+    ? 'Last revision posted at'
+    : 'Created at';
   const userLabel = user != null
     ? <a href={userPageRoot(user)}>{user.name}</a>
     : <i>Unknown</i>;
 
+  if (locate === 'footer') {
+    return <p>{infoLabelForFooter} {date} by <UserPicture user={user} size="sm" /> {userLabel}</p>;
+  }
+
   return (
     <div className="d-flex align-items-center">
       <div className="mr-2">
         <UserPicture user={user} size="sm" />
       </div>
       <div>
-        <div>{infoLabel} {userLabel}</div>
+        <div>{infoLabelForSubNav} {userLabel}</div>
         <div className="text-muted text-date">{date}</div>
       </div>
     </div>
@@ -32,10 +41,12 @@ AuthorInfo.propTypes = {
   date: PropTypes.string.isRequired,
   user: PropTypes.object,
   mode: PropTypes.oneOf(['create', 'update']),
+  locate: PropTypes.oneOf(['subnav', 'footer']),
 };
 
 AuthorInfo.defaultProps = {
   mode: 'create',
+  locate: 'subnav',
 };
 
 

+ 1 - 2
src/client/js/components/Navbar/GrowiNavbar.jsx

@@ -76,10 +76,9 @@ class GrowiNavbar extends React.Component {
         {/* Navbar Right  */}
         <ul className="navbar-nav ml-auto">
           {this.renderNavbarRight()}
+          {crowi.confidential != null && this.renderConfidential()}
         </ul>
 
-        {crowi.confidential != null && this.renderConfidential()}
-
         { isSearchServiceConfigured && !isDeviceSmallerThanMd && (
           <div className="grw-global-search grw-global-search-top position-absolute">
             <GlobalSearch />

+ 11 - 7
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
@@ -71,18 +71,22 @@ const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 
   const {
-    pageId, isLiked, pageUser,
+    pageUser, shareLinkId,
   } = pageContainer.state;
 
+  const isSharedPage = useMemo(() => {
+    return shareLinkId != null;
+  }, [shareLinkId]);
+
   return (
     <>
-      {pageUser == null && (
+      {pageUser == null && !isSharedPage && (
       <span className="mr-2">
-        <LikeButton pageId={pageId} isLiked={isLiked} />
+        <LikeButton />
       </span>
       )}
       <span>
-        <BookmarkButton pageId={pageId} crowi={appContainer} />
+        <BookmarkButton crowi={appContainer} />
       </span>
     </>
   );
@@ -155,10 +159,10 @@ const GrowiSubNavigation = (props) => {
         { (!isCompactMode && !isUserPage && !isPageNotFound && !isPageForbidden) && (
           <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3">
             <li className="pb-1">
-              <AuthorInfo user={creator} date={createdAt} />
+              <AuthorInfo user={creator} date={createdAt} locate="subnav" />
             </li>
             <li className="mt-1 pt-1 border-top">
-              <AuthorInfo user={revisionAuthor} date={updatedAt} mode="update" />
+              <AuthorInfo user={revisionAuthor} date={updatedAt} mode="update" locate="subnav" />
             </li>
           </ul>
         ) }

+ 1 - 1
src/client/js/components/Page.jsx

@@ -134,7 +134,7 @@ class Page extends React.Component {
     const { markdown } = pageContainer.state;
 
     return (
-      <div className={`${isMobile && 'page-mobile'}`}>
+      <div className={`mb-5 ${isMobile ? 'page-mobile' : ''}`}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
 
         { isLoggedIn && (

+ 1 - 1
src/client/js/components/Page/ShareLinkAlert.jsx

@@ -41,7 +41,7 @@ const ShareLinkAlert = (props) => {
   }
 
   return (
-    <p className={`alert alert-${specifyColor()} py-3 px-4`}>
+    <p className={`alert alert-${specifyColor()} py-3 px-4 d-edit-none`}>
       <i className="icon-fw icon-link"></i>
       {(expiredAt === '' ? <span>{t('page_page.notice.no_deadline')}</span>
       // eslint-disable-next-line react/no-danger

+ 6 - 4
src/client/js/components/PageAccessoriesModalControl.jsx

@@ -19,7 +19,8 @@ const PageAccessoriesModalControl = (props) => {
   const { t, pageAccessoriesContainer, isGuestUserMode } = props;
 
   return (
-    <div className="grw-page-accessories-control d-flex align-items-center pb-1">
+    <div className="grw-page-accessories-control d-flex align-items-center justify-content-between pb-1">
+
       <button
         type="button"
         className="btn btn-link grw-btn-page-accessories"
@@ -67,9 +68,10 @@ const PageAccessoriesModalControl = (props) => {
         </UncontrolledTooltip>
       )}
 
-      <span className="border-left grw-border-vr mx-1">&nbsp;</span>
-
-      <SeenUserInfo />
+      <div className="d-flex align-items-center">
+        <span className="border-left grw-border-vr">&nbsp;</span>
+        <SeenUserInfo />
+      </div>
     </div>
   );
 };

+ 1 - 1
src/client/js/components/PageComments.jsx

@@ -148,7 +148,7 @@ class PageComments extends React.Component {
     }
 
     return (
-      <div key={commentId} className={`mb-5 ${rootClassNames}`}>
+      <div key={commentId} className={rootClassNames}>
         <Comment
           comment={comment}
           deleteBtnClicked={this.confirmToDeleteComment}

+ 39 - 0
src/client/js/components/PageContentFooter.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import AuthorInfo from './Navbar/AuthorInfo';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import { withUnstatedContainers } from './UnstatedUtils';
+
+const PageContentFooter = (props) => {
+  const { pageContainer } = props;
+  const {
+    createdAt, creator, updatedAt, revisionAuthor,
+  } = pageContainer.state;
+
+  return (
+    <div className="page-content-footer py-4 d-edit-none d-print-none">
+      <div className="container-lg">
+        <div className="page-meta">
+          <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
+          <AuthorInfo user={revisionAuthor} date={updatedAt} mode="update" locate="footer" />
+        </div>
+      </div>
+    </div>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageContentFooterWrapper = withUnstatedContainers(PageContentFooter, [AppContainer, PageContainer]);
+
+
+PageContentFooter.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default PageContentFooterWrapper;

+ 21 - 9
src/client/js/components/TableOfContents.jsx

@@ -20,7 +20,7 @@ const logger = loggerFactory('growi:TableOfContents');
  */
 const TableOfContents = (props) => {
 
-  const { pageContainer, navigationContainer } = props;
+  const { t, pageContainer, navigationContainer } = props;
   const { pageUser } = pageContainer.state;
   const isUserPage = pageUser != null;
 
@@ -59,14 +59,24 @@ const TableOfContents = (props) => {
       stickyElemSelector=".grw-side-contents-sticky-container"
       calcViewHeightFunc={calcViewHeight}
     >
-      <div
-        id="revision-toc-content"
-        className="revision-toc-content"
-        // eslint-disable-next-line react/no-danger
-        dangerouslySetInnerHTML={{
-        __html: tocHtml,
-      }}
-      />
+      { tocHtml !== ''
+      ? (
+        <div
+          id="revision-toc-content"
+          className="revision-toc-content mb-3"
+          // eslint-disable-next-line react/no-danger
+          dangerouslySetInnerHTML={{ __html: tocHtml }}
+        />
+      )
+      : (
+        <div
+          id="revision-toc-content"
+          className="revision-toc-content mb-2"
+        >
+          <span className="text-muted">({t('page_table_of_contents.empty')})</span>
+        </div>
+      ) }
+
     </StickyStretchableScroller>
   );
 
@@ -78,6 +88,8 @@ const TableOfContents = (props) => {
 const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageContainer, NavigationContainer]);
 
 TableOfContents.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };

+ 1 - 1
src/client/js/components/User/UserInfo.jsx

@@ -12,7 +12,7 @@ const UserInfo = (props) => {
   }
 
   return (
-    <div className="grw-users-info d-flex align-items-center d-edit-none pb-2 border-bottom">
+    <div className="grw-users-info d-flex align-items-center d-edit-none mb-5 pb-3 border-bottom">
       <UserPicture user={pageUser} />
 
       <div className="users-meta">

+ 0 - 24
src/client/js/legacy/crowi.js

@@ -156,36 +156,12 @@ Crowi.highlightSelectedSection = function(hash) {
 
 $(() => {
   const pageId = $('#content-main').data('page-id');
-  // const revisionId = $('#content-main').data('page-revision-id');
-  // const revisionCreatedAt = $('#content-main').data('page-revision-created');
-  // const currentUser = $('#content-main').data('current-user');
   const isSeen = $('#content-main').data('page-is-seen');
 
   $('[data-toggle="popover"]').popover();
   $('[data-toggle="tooltip"]').tooltip();
   $('[data-tooltip-stay]').tooltip('show');
 
-  $('#toggle-crowi-sidebar').click((e) => {
-    const $body = $('body');
-    if ($body.hasClass('aside-hidden')) {
-      $body.removeClass('aside-hidden');
-      $.cookie('aside-hidden', 0, { expires: 30, path: '/' });
-    }
-    else {
-      $body.addClass('aside-hidden');
-      $.cookie('aside-hidden', 1, { expires: 30, path: '/' });
-    }
-    return false;
-  });
-
-  if ($.cookie('aside-hidden') === 1) {
-    $('body').addClass('aside-hidden');
-  }
-
-  $('.copy-link').on('click', function() {
-    $(this).select();
-  });
-
   if (pageId) {
 
     if (!isSeen) {

+ 16 - 18
src/client/js/services/PageContainer.js

@@ -101,9 +101,15 @@ export default class PageContainer extends Container {
     interceptorManager.addInterceptor(new DrawioInterceptor(appContainer), 20);
     interceptorManager.addInterceptor(new RestoreCodeBlockInterceptor(appContainer), 900); // process as late as possible
 
-    this.retrieveSeenUsers();
     this.initStateMarkdown();
-    this.initStateOthers();
+    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
+
+    // skip if shared page
+    if (this.state.shareLinkId == null) {
+      this.retrieveSeenUsers();
+      this.retrieveLikeInfo();
+      this.retrieveBookmarkInfo();
+    }
 
     this.setTocHtml = this.setTocHtml.bind(this);
     this.save = this.save.bind(this);
@@ -155,13 +161,6 @@ export default class PageContainer extends Container {
     this.checkAndUpdateImageUrlCached(users);
   }
 
-  async initStateOthers() {
-
-    this.retrieveLikeInfo();
-    this.retrieveBookmarkInfo();
-    this.checkAndUpdateImageUrlCached(this.state.likerUsers);
-  }
-
   async retrieveLikeInfo() {
     const like = await this.appContainer.apiv3Get('/page/like-info', { _id: this.state.pageId });
     this.setState({
@@ -180,14 +179,11 @@ export default class PageContainer extends Container {
   }
 
   async retrieveBookmarkInfo() {
-    const response = await this.appContainer.apiv3Get('/bookmarks', { pageId: this.state.pageId });
-    if (response.data.bookmarks != null) {
-      this.setState({ isBookmarked: true });
-    }
-    else {
-      this.setState({ isBookmarked: false });
-    }
-    this.setState({ sumOfBookmarks: response.data.sumOfBookmarks });
+    const response = await this.appContainer.apiv3Get('/bookmarks/info', { pageId: this.state.pageId });
+    this.setState({
+      sumOfBookmarks: response.data.sumOfBookmarks,
+      isBookmarked: response.data.isBookmarked,
+    });
   }
 
   async toggleBookmark() {
@@ -247,6 +243,8 @@ export default class PageContainer extends Container {
       revisionIdHackmdSynced: page.revisionHackmdSynced,
       hasDraftOnHackmd: page.hasDraftOnHackmd,
       markdown: page.revision.body,
+      createdAt: page.createdAt,
+      updatedAt: page.updatedAt,
     };
     if (tags != null) {
       newState.tags = tags;
@@ -256,7 +254,7 @@ export default class PageContainer extends Container {
     // PageEditor component
     const pageEditor = this.appContainer.getComponentInstance('PageEditor');
     if (pageEditor != null) {
-      if (editorMode !== 'builtin') {
+      if (editorMode !== 'edit') {
         pageEditor.updateEditorValue(newState.markdown);
       }
     }

+ 7 - 3
src/client/js/services/TagContainer.js

@@ -39,11 +39,15 @@ export default class TagContainer extends Container {
       return;
     }
 
-    const { pageId, templateTagData } = pageContainer.state;
+    const { pageId, templateTagData, shareLinkId } = pageContainer.state;
+
+    if (shareLinkId != null) {
+      return;
+    }
 
     let tags = [];
-    // when the page exists
-    if (pageId != null) {
+    // when the page exists or shared page
+    if (pageId != null && shareLinkId == null) {
       const res = await this.appContainer.apiGet('/pages.getPageTag', { pageId });
       tags = res.tags;
     }

+ 0 - 41
src/client/styles/scss/_attachments.scss

@@ -1,44 +1,3 @@
-.page-attachments-row {
-  border-top: solid 1px transparent;
-}
-
-.page-attachments {
-  li.attachment {
-    list-style: none;
-  }
-
-  .attachment-userpicture {
-    line-height: 1.7em;
-    vertical-align: bottom;
-  }
-}
-
-.page-attachments,
-.page-meta {
-  font-size: 0.95em;
-
-  .attachment-in-use {
-    padding: 1px 5px;
-    margin: 0 0 0 4px;
-  }
-
-  .attachment-filetype {
-    padding: 1px 5px;
-    margin: 0 0 0 4px;
-    font-weight: normal;
-  }
-
-  .attachment-download {
-    margin: 0 0 0 4px;
-    cursor: pointer;
-  }
-
-  .attachment-delete {
-    margin: 0 0 0 4px;
-    cursor: pointer;
-  }
-}
-
 .attachment-delete-modal {
   .attachment-delete-image {
     text-align: center;

+ 4 - 10
src/client/styles/scss/_layout.scss

@@ -36,6 +36,10 @@ body {
   }
 }
 
+.grw-side-contents-container {
+  margin-left: 30px;
+}
+
 .grw-side-contents-sticky-container {
   position: sticky;
   // growisubnavigation + grw-navbar-boder
@@ -103,16 +107,6 @@ body {
       margin-bottom: 20px;
       font-size: 0.9em;
       border: solid 1px $gray-400;
-
-      .revision-toc-head {
-        display: inline-block;
-        float: none;
-      }
-
-      .revision-toc-content.collapse {
-        display: block;
-        height: auto;
-      }
     }
 
     .meta {

+ 1 - 2
src/client/styles/scss/_page-accessories-control.scss

@@ -4,8 +4,7 @@
   border-bottom: 1px solid transparent;
 
   .grw-btn-page-accessories {
-    padding: 0.375rem 0.5rem;
-    margin: 0 0.2rem;
+    padding: 0.375rem;
 
     svg {
       width: 16px;

+ 6 - 0
src/client/styles/scss/_page-content-footer.scss

@@ -0,0 +1,6 @@
+.page-content-footer {
+  border-top: solid 1px transparent;
+  .page-meta {
+    font-size: 0.95em;
+  }
+}

+ 1 - 26
src/client/styles/scss/_page.scss

@@ -1,32 +1,7 @@
 // import diff2html styles
 @import '~diff2html/bundles/css/diff2html.min.css';
 
-.main-container {
-  .url-line {
-    font-size: 1rem;
-    color: $gray-400;
-  }
-
-  h1.title {
-    margin-top: 0;
-    margin-bottom: 0;
-
-    .d-flex {
-      flex-wrap: wrap; // for long page path
-    }
-
-    // crowi layout only
-    a.last-path {
-      color: $gray-300;
-
-      &:hover {
-        color: inherit;
-      }
-    }
-  }
-}
-
-.main .content-main .revision-history {
+.revision-history {
   .revision-history-list {
     .revision-history-outer {
       // add border-top except of first element

+ 0 - 10
src/client/styles/scss/_page_list.scss

@@ -72,13 +72,3 @@ body .page-list {
     background-color: $gray-300;
   }
 }
-
-.grw-page-list-m {
-  .grw-page-list-title-m {
-    svg {
-      width: 35px;
-      height: 35px;
-      margin-bottom: 6px;
-    }
-  }
-}

+ 1 - 1
src/client/styles/scss/_subnav.scss

@@ -106,7 +106,7 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   z-index: $zindex-sticky - 5;
 
   .grw-subnav {
-    box-shadow: 0px 6px 6px 3px rgba(black, 0.15);
+    box-shadow: 0px 0px 6px 3px rgba(black, 0.15);
   }
 }
 

+ 10 - 0
src/client/styles/scss/_user.scss

@@ -43,3 +43,13 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
     }
   }
 }
+
+.user-page {
+  .grw-user-page-header {
+    svg {
+      width: 35px;
+      height: 35px;
+      margin-bottom: 6px;
+    }
+  }
+}

+ 0 - 6
src/client/styles/scss/_user_growi.scss

@@ -1,6 +0,0 @@
-.growi .user-page {
-  .revision-toc {
-    position: sticky;
-    top: 105px;
-  }
-}

+ 1 - 1
src/client/styles/scss/atoms/_buttons.scss

@@ -1,5 +1,5 @@
 .btn.btn-like {
-  @include button-outline-variant($secondary, lighten($info, 15%), rgba(lighten($info, 10%), 0.5), rgba(lighten($info, 10%), 0.5));
+  @include button-outline-variant($secondary, lighten($info, 15%), rgba(lighten($info, 10%), 0.15), rgba(lighten($info, 10%), 0.5));
   &:not(:disabled):not(.disabled):active,
   &:not(:disabled):not(.disabled).active {
     color: lighten($info, 15%);

+ 1 - 1
src/client/styles/scss/style-app.scss

@@ -35,6 +35,7 @@
 @import 'draft';
 @import 'editor-attachment';
 @import 'editor-navbar';
+@import 'page-content-footer';
 @import 'handsontable';
 @import 'layout';
 @import 'login';
@@ -56,7 +57,6 @@
 @import 'tag';
 @import 'toc';
 @import 'user';
-@import 'user_growi';
 @import 'staff_credit';
 @import 'waves';
 @import 'wiki';

+ 2 - 15
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -349,10 +349,8 @@ body.on-edit {
   }
 }
 
-.growi .main {
-  .page-comments-row {
-    background: $bgcolor-subnav;
-  }
+.page-comments-row {
+  background: $bgcolor-subnav;
 }
 
 /*
@@ -364,14 +362,3 @@ body.on-edit {
     background-color: $bgcolor-tags;
   }
 }
-
-/*
- * GROWI user page
- */
-.grw-page-list-m {
-  .grw-page-list-title-m {
-    svg {
-      fill: $color-global;
-    }
-  }
-}

+ 2 - 15
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -279,10 +279,8 @@ $table-hover-bg: $bgcolor-table-hover;
   }
 }
 
-.growi .main {
-  .page-comments-row {
-    background: $bgcolor-subnav;
-  }
+.page-comments-row {
+  background: $bgcolor-subnav;
 }
 
 /*
@@ -294,14 +292,3 @@ $table-hover-bg: $bgcolor-table-hover;
     background-color: $bgcolor-tags;
   }
 }
-
-/*
- * GROWI user page
- */
-.grw-page-list-m {
-  .grw-page-list-title-m {
-    svg {
-      fill: $color-global;
-    }
-  }
-}

+ 3 - 3
src/client/styles/scss/theme/_apply-colors.scss

@@ -457,7 +457,7 @@ body.on-edit {
 /*
  * GROWI comment form
  */
-.growi .main {
+.page-comments {
   .page-comment .page-comment-main,
   .page-comment-form .comment-form-main {
     background-color: $bgcolor-global;
@@ -509,9 +509,9 @@ mark.rbt-highlight-text {
 }
 
 /*
- * GROWI page attachments
+ * GROWI page content footer
  */
-.page-attachments-row {
+.page-content-footer {
   background-color: darken($bgcolor-global, 2%);
   border-top-color: $border-color-theme;
 }

+ 1 - 1
src/client/styles/scss/theme/kibela.scss

@@ -32,7 +32,7 @@ $lightthemecolor: rgba(181, 203, 247, 0.61);
   border-radius: 0.35em;
 }
 
-.page-attachments-row {
+.page-content-footer {
   margin-top: 30px;
 }
 

+ 4 - 0
src/client/styles/scss/theme/spring.scss

@@ -146,6 +146,10 @@ html[dark] {
   h1,
   h2 {
     color: $subthemecolor;
+
+    svg {
+      fill: $subthemecolor;
+    }
   }
 
   .nav.nav-tabs {

+ 2 - 2
src/server/middlewares/login-required.js

@@ -47,13 +47,13 @@ module.exports = (crowi, isGuestAllowed = false, fallback = null) => {
     const path = req.path || '';
     if (path.match(/^\/_api\/.+$/)) {
       if (fallback != null) {
-        return fallback(req, res);
+        return fallback(req, res, next);
       }
       return res.sendStatus(403);
     }
 
     if (fallback != null) {
-      return fallback(req, res);
+      return fallback(req, res, next);
     }
     req.session.redirectTo = req.originalUrl;
     return res.redirect('/login');

+ 43 - 36
src/server/routes/apiv3/bookmarks.js

@@ -50,11 +50,23 @@ const router = express.Router();
  *          bool:
  *            type: boolean
  *            description: boolean for bookmark status
+ *
+ *      BookmarkInfo:
+ *        description: BookmarkInfo
+ *        type: object
+ *        properties:
+ *          sumOfBookmarks:
+ *            type: number
+ *            description: how many people bookmarked the page
+ *          isBookmarked:
+ *            type: boolean
+ *            description: Whether the request user bookmarked (will be returned if the user is included in the request)
  */
 
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequired = require('../../middlewares/login-required')(crowi);
+  const loginRequiredStrictly = require('@server/middlewares/login-required')(crowi);
+  const loginRequired = require('../../middlewares/login-required')(crowi, true);
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
@@ -73,12 +85,12 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /bookmarks:
+   *    /bookmarks/info:
    *      get:
    *        tags: [Bookmarks]
-   *        summary: /bookmarks
-   *        description: Get bookmarked status
-   *        operationId: getBookmarkedStatus
+   *        summary: /bookmarks/info
+   *        description: Get bookmarked info
+   *        operationId: getBookmarkedInfo
    *        parameters:
    *          - name: pageId
    *            in: query
@@ -87,24 +99,41 @@ module.exports = (crowi) => {
    *              type: string
    *        responses:
    *          200:
-   *            description: Succeeded to get bookmarked status.
+   *            description: Succeeded to get bookmark info.
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/Bookmark'
+   *                  $ref: '#/components/schemas/BookmarkInfo'
    */
-  router.get('/', accessTokenParser, loginRequired, validator.bookmarkInfo, async(req, res) => {
+  router.get('/info', accessTokenParser, loginRequired, validator.bookmarkInfo, apiV3FormValidator, async(req, res) => {
+    const { user } = req;
     const { pageId } = req.query;
 
+    const responsesParams = {};
+
     try {
-      const bookmarks = await Bookmark.findByPageIdAndUserId(pageId, req.user);
-      const sumOfBookmarks = await Bookmark.countByPageId(pageId);
-      return res.apiv3({ bookmarks, sumOfBookmarks });
+      responsesParams.sumOfBookmarks = await Bookmark.countByPageId(pageId);
     }
     catch (err) {
-      logger.error('get-bookmark-failed', err);
+      logger.error('get-bookmark-count-failed', err);
       return res.apiv3Err(err, 500);
     }
+
+    // guest user only get bookmark count
+    if (user == null) {
+      return res.apiv3(responsesParams);
+    }
+
+    try {
+      const bookmark = await Bookmark.findByPageIdAndUserId(pageId, user._id);
+      responsesParams.isBookmarked = (bookmark != null);
+      return res.apiv3(responsesParams);
+    }
+    catch (err) {
+      logger.error('get-bookmark-state-failed', err);
+      return res.apiv3Err(err, 500);
+    }
+
   });
 
   // select page from bookmark where userid = userid
@@ -152,7 +181,7 @@ module.exports = (crowi) => {
     query('limit').if(value => value != null).isInt({ max: 300 }).withMessage('You should set less than 300 or not to set limit.'),
   ];
 
-  router.get('/:userId', accessTokenParser, loginRequired, validator.myBookmarkList, apiV3FormValidator, async(req, res) => {
+  router.get('/:userId', accessTokenParser, loginRequiredStrictly, validator.myBookmarkList, apiV3FormValidator, async(req, res) => {
     const { userId } = req.params;
     const page = req.query.page;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
@@ -213,7 +242,7 @@ module.exports = (crowi) => {
    *                schema:
    *                  $ref: '#/components/schemas/Bookmark'
    */
-  router.put('/', accessTokenParser, loginRequired, csrf, validator.bookmarks, apiV3FormValidator, async(req, res) => {
+  router.put('/', accessTokenParser, loginRequiredStrictly, csrf, validator.bookmarks, apiV3FormValidator, async(req, res) => {
     const { pageId, bool } = req.body;
 
     let bookmark;
@@ -240,27 +269,5 @@ module.exports = (crowi) => {
     return res.apiv3({ bookmark });
   });
 
-  /**
-   * @swagger
-   *
-   *    /count-bookmarks:
-   *      get:
-   *        tags: [Bookmarks]
-   *        summary: /bookmarks
-   *        description: Count bookmsrks
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                $ref: '#/components/schemas/BookmarkParams'
-   *        responses:
-   *          200:
-   *            description: Succeeded to count bookmarks.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  $ref: '#/components/schemas/Bookmark'
-   */
-
   return router;
 };

+ 0 - 3
src/server/views/admin/app.html

@@ -13,6 +13,3 @@
 {% block content_main %}
   <div id="admin-app"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 2
src/server/views/admin/customize.html

@@ -22,5 +22,3 @@
 </div>
 <div id="admin-customize" class="admin-customize"></div>
 {% endblock content_main %}
-
-{% block content_footer %} {% endblock content_footer %}

+ 0 - 3
src/server/views/admin/export.html

@@ -9,6 +9,3 @@
 {% block content_main %}
 <div id="admin-export-page" class="admin-export"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 5
src/server/views/admin/external-accounts.html

@@ -9,8 +9,3 @@
 {% block content_main %}
 <div id="admin-external-account-setting"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-

+ 0 - 3
src/server/views/admin/global-notification-detail.html

@@ -10,6 +10,3 @@
 <div id="admin-global-notification-setting"
     data-global-notification="{{ globalNotification|json }}"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 3
src/server/views/admin/importer.html

@@ -9,6 +9,3 @@
 {% block content_main %}
 <div id="admin-importer" class="admin-importer"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 3
src/server/views/admin/index.html

@@ -9,6 +9,3 @@
 {% block content_main %}
 <div id="admin-home"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 10
src/server/views/admin/markdown.html

@@ -9,13 +9,3 @@
 {% block content_main %}
 <div id="admin-markdown-setting"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-
-
-
-
-
-

+ 0 - 3
src/server/views/admin/notification.html

@@ -9,6 +9,3 @@
 {% block content_main %}
 <div id="admin-notification-setting" class="admin-notification"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 3
src/server/views/admin/search.html

@@ -9,6 +9,3 @@
 {% block content_main %}
   <div id ="admin-full-text-search-management"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 3
src/server/views/admin/security.html

@@ -9,6 +9,3 @@
 {% block content_main %}
 <div id="admin-security-setting" class="admin-security"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 3
src/server/views/admin/user-group-detail.html

@@ -13,6 +13,3 @@
 >
 </div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}

+ 0 - 5
src/server/views/admin/user-groups.html

@@ -9,8 +9,3 @@
 {% block content_main %}
 <div id ="admin-user-group-page"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-

+ 0 - 5
src/server/views/admin/users.html

@@ -9,8 +9,3 @@
 {% block content_main %}
 <div id ="admin-user-page"></div>
 {% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-

+ 25 - 19
src/server/views/layout-growi/base/layout.html

@@ -7,29 +7,35 @@
 {% endblock %}
 
 {% block layout_main %}
+<div class="h-100 d-flex flex-column justify-content-between">
 
-{% block content_header_wrapper %}
-<header class="py-0">
-  {% block content_header %}
-    <div id="grw-subnav-container"></div>
+  {% block content_header_wrapper %}
+    <header class="py-0">
+      {% block content_header %}
+        <div id="grw-subnav-container"></div>
+      {% endblock %}
+      <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
+      <div id="grw-subnav-sticky-trigger" class="sticky-top"></div>
+      <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
+    </header>
   {% endblock %}
-</header>
-<div id="grw-subnav-switcher-container" class="d-edit-none"></div>
-<div id="grw-subnav-sticky-trigger" class="sticky-top"></div>
-<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
-{% endblock %}
 
-<div id="main" class="main {% if page %}{{ css.grant(page) }}{% endif %} {% block main_css_class %}{% endblock %}">
-  {% block content_main_before %}
-  {% endblock %}
+  <div class="flex-grow-1">
+    <div id="main" class="main {% if page %}{{ css.grant(page) }}{% endif %}">
+      {% block content_main_before %}
+      {% endblock %}
 
-  {% block content_main %}
-  {% endblock content_main %}
+      {% block content_main %}
+      {% endblock content_main %}
 
-  {% block content_main_after %}
-  {% endblock %}
-</div><!-- /.main -->
+      {% block content_main_after %}
+      {% endblock %}
+    </div>
+  </div>
+
+  <footer class="footer">
+    {% block content_footer %}{% endblock %}
+  </footer>
 
-<footer class="footer">
-</footer>
+</div>
 {% endblock %} {# layout_main #}

+ 1 - 5
src/server/views/layout-growi/forbidden.html

@@ -9,11 +9,7 @@
 
 {% block content_main %}
   <div class="container-lg">
-    <div class="row">
-      <div class="col">
-        {% include '../widget/forbidden_content.html' %}
-      </div>
-    </div>
+    {% include '../widget/forbidden_content.html' %}
   </div>
 {% endblock %}
 

+ 1 - 5
src/server/views/layout-growi/not_creatable.html

@@ -10,11 +10,7 @@
 
 {% block content_main %}
   <div class="container-lg">
-    <div class="row">
-      <div class="col">
-        {% include '../widget/not_creatable_content.html' %}
-      </div>
-    </div>
+    {% include '../widget/not_creatable_content.html' %}
   </div>
 {% endblock %}
 

+ 1 - 5
src/server/views/layout-growi/not_found.html

@@ -10,11 +10,7 @@
 
 {% block content_main %}
   <div class="container-lg">
-    <div class="row">
-      <div class="col">
-        {% include '../widget/not_found_content.html' %}
-      </div>
-    </div>
+    {% include '../widget/not_found_content.html' %}
   </div>
 {% endblock %}
 

+ 2 - 7
src/server/views/layout-growi/page.html

@@ -13,16 +13,11 @@
   </div>
 {% endblock %}
 
-
-{% block content_main_after %}
+{% block content_footer %}
   {% include 'widget/comments.html' %}
-
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
+  <div id="page-content-footer"></div>
 {% endblock %}
 
-
 {% block body_end %}
   <div id="presentation-layer" class="fullscreen-layer">
     <div id="presentation-container"></div>

+ 3 - 3
src/server/views/layout-growi/page_list.html

@@ -18,11 +18,11 @@
       <div id="trash-page-list"></div>
     </div>
   {% endif %}
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
 {% endblock %}
 
+{% block content_footer %}
+  <div id="page-content-footer"></div>
+{% endblock %}
 
 {% block body_end %}
   <div id="presentation-layer" class="fullscreen-layer">

+ 5 - 13
src/server/views/layout-growi/user_page.html

@@ -1,27 +1,21 @@
 {% extends 'page.html' %}
 
-{% block main_css_class %}
-  {% parent %}
-  user-page
-{% endblock %}
-
 {% block content_main %}
-  <div class="container-lg">
+  <div class="container-lg user-page">
 
     {% include '../widget/page_content.html' %}
 
   </div>
 {% endblock %}
 
-
-{% block content_main_after %}
+{% block content_footer %}
   {% include 'widget/comments.html' %}
 
   {% if page %}
     <div class="container-lg">
 
       <div class="grw-page-list-m mt-5 pb-5 d-edit-none">
-        <h2 class="grw-page-list-title-m border-bottom pb-2 mb-3" id="bookmarks-list">
+        <h2 class="grw-user-page-header border-bottom pb-2 mb-3" id="bookmarks-list">
           <i id="user-bookmark-icon"></i>
           Bookmarks
         </h2>
@@ -32,7 +26,7 @@
       </div>
 
       <div class="grw-page-list-m mt-5 pb-5 d-edit-none">
-        <h2 class="grw-page-list-title-m border-bottom pb-2 mb-3" id="recently-created-list">
+        <h2 class="grw-user-page-header border-bottom pb-2 mb-3" id="recently-created-list">
           <i id="recent-created-icon"></i>
           Recently Created
         </h2>
@@ -45,7 +39,5 @@
     </div>
   {% endif %}
 
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
+  <div id="page-content-footer"></div>
 {% endblock %}

+ 1 - 4
src/server/views/layout/admin.html

@@ -16,7 +16,7 @@
 </header>
 {% endblock %}
 
-<div id="main" class="main {% block main_css_class %}{% endblock %}">
+<div id="main" class="main">
 
   <div class="container-fluid">
     <div class="row">
@@ -34,7 +34,4 @@
     </div>
   </div>
 </div><!-- /.main -->
-
-<footer class="footer">
-</footer>
 {% endblock %} {# layout_main #}

+ 15 - 12
src/server/views/me/drafts.html

@@ -1,18 +1,21 @@
-{% extends '../layout-growi/base/layout.html' %}
+{% extends '../layout/layout.html' %}
 
 {% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('My Drafts')) }}{% endblock %}
 
-{% block content_header %}
+{% block layout_main %}
+
+{% block content_header_wrapper %}
+<header class="py-3">
+  <div class="container-fluid">
+    <h1 class="title">{{ t('My Drafts') }}</h1>
+  </div>
+</header>
+<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
 {% endblock %}
 
-{% block content_main %}
-<div id="content-main" class="content-main container">
-  <div id="my-drafts"></div>
+<div id="main" class="main">
+  <div id="content-main" class="content-main container-lg">
+    <div id="my-drafts"></div>
+  </div>
 </div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-{% block layout_footer %}
-{% endblock layout_footer %}
+{% endblock %}

+ 15 - 13
src/server/views/me/index.html

@@ -1,19 +1,21 @@
-{% extends '../layout-growi/base/layout.html' %}
+{% extends '../layout/layout.html' %}
 
 {% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('User Settings')) }}{% endblock %}
 
-{% block html_base_css %}user-settings-page{% endblock %}
+{% block layout_main %}
 
-{% block content_header %}
-<h1 class="title">{{ t('User Settings') }}</h1>
+{% block content_header_wrapper %}
+<header class="py-3">
+  <div class="container-fluid">
+    <h1 class="title">{{ t('User Settings') }}</h1>
+  </div>
+</header>
+<div id="grw-fav-sticky-trigger" class="sticky-top"></div>
 {% endblock %}
 
-{% block content_main %}
-<div class="content-main" id="personal-setting"></div>
-{% endblock content_main %}
-
-{% block content_footer %}
-{% endblock content_footer %}
-
-{% block layout_footer %}
-{% endblock layout_footer %}
+<div id="main" class="main">
+  <div id="content-main" class="content-main container-lg">
+    <div class="content-main" id="personal-setting"></div>
+  </div>
+</div>
+{% endblock %}

+ 0 - 3
src/server/views/search.html

@@ -23,7 +23,4 @@
   </div>
 
 </div><!-- /.container-fluid -->
-
-<footer class="footer">
-</footer>
 {% endblock %} {# layout_main #}

+ 0 - 3
src/server/views/tags.html

@@ -16,7 +16,4 @@
     </div>
   </div>
 </div><!-- /.container-fluid -->
-
-<footer class="footer">
-</footer>
 {% endblock %} {# layout_main #}

+ 0 - 10
src/server/views/widget/page_attachments.html

@@ -1,10 +0,0 @@
-<div class="page-attachments-row mt-5 py-4 d-edit-none d-print-none">
-  <div class="container-lg">
-    <div class="page-attachments" id="page-attachment"></div>
-
-    <p class="page-meta">
-      <p>Last revision posted at {{ page.revision.createdAt|datetz('Y-m-d H:i:s') }} by <a href="/user/{{ page.revision.author.username }}"><img src="{{ page.revision.author|picture }}" class="picture picture-sm rounded-circle"> {{ page.revision.author.name }}</a></p>
-      <p>Created at {{ page.createdAt|datetz('Y-m-d H:i:s') }} by <a href="/user/{{ page.creator.username }}"><img src="{{ page.creator|default(page.creator)|picture }}" class="picture picture-sm rounded-circle"> {{ page.creator.name }}</a></p>
-    </p>
-  </div>
-</div>

+ 5 - 4
src/server/views/widget/page_content.html

@@ -54,17 +54,18 @@
   <div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
 </div>
 
-<div class="d-none d-lg-block d-editor-none grw-side-contents-container ml-4">
+{% if revision %}
+<div class="d-none d-lg-block d-edit-none grw-side-contents-container">
   <div class="grw-side-contents-sticky-container">
     <div id="page-accessories" class="page-accessories"></div>
-    <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="123">
-      <div id="revision-toc-content" class="revision-toc-content"></div>
-    </div>
+    <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="123"></div>
     {% if pageUser %}
       <div id="grw-user-contents-links"></div>
     {% endif %}
   </div>
 </div>
+{% endif %}
+
 
 <div id="grw-page-status-alert-container"></div>
 

+ 2 - 2
src/test/middlewares/login-required.test.js

@@ -228,7 +228,7 @@ describe('loginRequired', () => {
       expect(res.redirect).not.toHaveBeenCalled();
       expect(res.sendStatus).not.toHaveBeenCalled();
       expect(fallbackMock).toHaveBeenCalledTimes(1);
-      expect(fallbackMock).toHaveBeenCalledWith(req, res);
+      expect(fallbackMock).toHaveBeenCalledWith(req, res, next);
       expect(result).toBe('fallback');
     });
 
@@ -242,7 +242,7 @@ describe('loginRequired', () => {
       expect(res.sendStatus).not.toHaveBeenCalled();
       expect(res.redirect).not.toHaveBeenCalled();
       expect(fallbackMock).toHaveBeenCalledTimes(1);
-      expect(fallbackMock).toHaveBeenCalledWith(req, res);
+      expect(fallbackMock).toHaveBeenCalledWith(req, res, next);
       expect(result).toBe('fallback');
     });