Browse Source

Merge branch 'master' into feat/duplicate-with-subordinate-page

itizawa 5 years ago
parent
commit
96ca04ea24

+ 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 - 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 />,

+ 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 => (

+ 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 />

+ 9 - 5
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,12 +71,16 @@ const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
 const PageReactionButtons = ({ appContainer, pageContainer }) => {
 
   const {
-    pageId, isLiked, pageUser,
+    pageId, isLiked, 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} />
       </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/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

+ 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 mt-5 py-4 d-edit-none d-print-none">
+      <div className="container-lg">
+        <p className="page-meta">
+          <AuthorInfo user={creator} date={createdAt} mode="create" locate="footer" />
+          <AuthorInfo user={revisionAuthor} date={updatedAt} mode="update" locate="footer" />
+        </p>
+      </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;

+ 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) {

+ 8 - 9
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({

+ 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 - 0
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

+ 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 - 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);
   }
 }
 

+ 1 - 0
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';

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

@@ -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;
 }
 

+ 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');

+ 1 - 4
src/server/views/layout-growi/page.html

@@ -16,10 +16,7 @@
 
 {% block content_main_after %}
   {% include 'widget/comments.html' %}
-
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
+  <div id="page-content-footer"></div>
 {% endblock %}
 
 

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

@@ -18,9 +18,7 @@
       <div id="trash-page-list"></div>
     </div>
   {% endif %}
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
+  <div id="page-content-footer"></div>
 {% endblock %}
 
 

+ 2 - 3
src/server/views/layout-growi/user_page.html

@@ -45,7 +45,6 @@
     </div>
   {% endif %}
 
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
+  <div id="page-content-footer"></div>
+
 {% endblock %}

+ 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>

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

@@ -54,7 +54,8 @@
   <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-editor-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">
@@ -65,6 +66,8 @@
     {% 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');
     });