Просмотр исходного кода

Merge branch 'master' into imprv/focus-link-input-when-open-link-edit-modal

# Conflicts:
#	src/client/js/components/SearchTypeahead.jsx
yusuketk 5 лет назад
Родитель
Сommit
b912d725ce

+ 67 - 0
.github/workflows/codeql-analysis.yml

@@ -0,0 +1,67 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ master, dev/*, release/current ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ master ]
+  # schedule:
+    - cron: '28 20 * * 6'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'javascript' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+        # Learn more:
+        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1

+ 6 - 1
CHANGES.md

@@ -1,7 +1,12 @@
 # CHANGES
 # CHANGES
 
 
-## v4.2.3-RC
+## v4.2.4
 
 
+* 
+
+## v4.2.3
+
+* Feature: Insert/edit links with GUI
 * Feature: Auto reconnecting to search service
 * Feature: Auto reconnecting to search service
 * Improvement: New style of params for Healthcheck API
 * Improvement: New style of params for Healthcheck API
 * Fix: Referencing attachments when `FILE_UPLOAD_DISABLED` is true
 * Fix: Referencing attachments when `FILE_UPLOAD_DISABLED` is true

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.2.3-RC",
+  "version": "4.2.4-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 25 - 0
src/client/js/components/SearchForm.jsx

@@ -13,10 +13,13 @@ class SearchForm extends React.Component {
 
 
     this.state = {
     this.state = {
       searchError: null,
       searchError: null,
+      isShownHelp: false,
     };
     };
 
 
     this.onSearchError = this.onSearchError.bind(this);
     this.onSearchError = this.onSearchError.bind(this);
     this.onChange = this.onChange.bind(this);
     this.onChange = this.onChange.bind(this);
+    this.onBlur = this.onBlur.bind(this);
+    this.onFocus = this.onFocus.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -40,12 +43,28 @@ class SearchForm extends React.Component {
     }
     }
   }
   }
 
 
+  onBlur() {
+    this.setState({
+      isShownHelp: false,
+    });
+
+    this.getHelpElement();
+  }
+
+  onFocus() {
+    this.setState({
+      isShownHelp: true,
+    });
+  }
+
   getHelpElement() {
   getHelpElement() {
     const { t, appContainer } = this.props;
     const { t, appContainer } = this.props;
+    const { isShownHelp } = this.state;
 
 
     const config = appContainer.getConfig();
     const config = appContainer.getConfig();
     const isReachable = config.isSearchServiceReachable;
     const isReachable = config.isSearchServiceReachable;
 
 
+
     if (!isReachable) {
     if (!isReachable) {
       return (
       return (
         <>
         <>
@@ -55,6 +74,10 @@ class SearchForm extends React.Component {
       );
       );
     }
     }
 
 
+    if (!isShownHelp) {
+      return null;
+    }
+
     return (
     return (
       <table className="table grw-search-table search-help m-0">
       <table className="table grw-search-table search-help m-0">
         <caption className="text-left text-primary p-2">
         <caption className="text-left text-primary p-2">
@@ -124,6 +147,8 @@ class SearchForm extends React.Component {
         placeholder={placeholder}
         placeholder={placeholder}
         helpElement={this.getHelpElement()}
         helpElement={this.getHelpElement()}
         keywordOnInit={this.props.keyword}
         keywordOnInit={this.props.keyword}
+        onBlur={this.onBlur}
+        onFocus={this.onFocus}
       />
       />
     );
     );
   }
   }

+ 4 - 0
src/client/js/components/SearchTypeahead.jsx

@@ -220,6 +220,8 @@ class SearchTypeahead extends React.Component {
           caseSensitive={false}
           caseSensitive={false}
           defaultSelected={defaultSelected}
           defaultSelected={defaultSelected}
           autoFocus={this.props.autoFocus}
           autoFocus={this.props.autoFocus}
+          onBlur={this.props.onBlur}
+          onFocus={this.props.onFocus}
         />
         />
         {resetFormButton}
         {resetFormButton}
       </div>
       </div>
@@ -242,6 +244,8 @@ SearchTypeahead.propTypes = {
   onSearchSuccess: PropTypes.func,
   onSearchSuccess: PropTypes.func,
   onSearchError:   PropTypes.func,
   onSearchError:   PropTypes.func,
   onChange:        PropTypes.func,
   onChange:        PropTypes.func,
+  onBlur:          PropTypes.func,
+  onFocus:         PropTypes.func,
   onSubmit:        PropTypes.func,
   onSubmit:        PropTypes.func,
   onInputChange:   PropTypes.func,
   onInputChange:   PropTypes.func,
   inputName:       PropTypes.string,
   inputName:       PropTypes.string,

+ 2 - 0
src/client/js/services/AdminUsersContainer.js

@@ -132,6 +132,8 @@ export default class AdminUsersContainer extends Container {
       sortOrder: this.state.sortOrder,
       sortOrder: this.state.sortOrder,
       selectedStatusList: Array.from(this.state.selectedStatusList),
       selectedStatusList: Array.from(this.state.selectedStatusList),
       searchText: this.state.searchText,
       searchText: this.state.searchText,
+      // Even if email is hidden, it will be displayed on admin page.
+      forceIncludeAttributes: ['email'],
     };
     };
     const { data } = await this.appContainer.apiv3.get('/users', params);
     const { data } = await this.appContainer.apiv3.get('/users', params);
 
 

+ 8 - 0
src/client/styles/scss/_on-edit.scss

@@ -134,6 +134,13 @@ body.on-edit {
 
 
     .grw-taglabels-container {
     .grw-taglabels-container {
       margin-bottom: 0;
       margin-bottom: 0;
+
+      // To scroll tags horizontally
+      .grw-tag-labels.form-inline {
+        flex-flow: row nowrap;
+        width: 100%;
+        overflow-x: scroll;
+      }
     }
     }
   }
   }
 
 
@@ -141,6 +148,7 @@ body.on-edit {
   .grw-subnav-left-side {
   .grw-subnav-left-side {
     overflow: hidden;
     overflow: hidden;
     .grw-path-nav-container {
     .grw-path-nav-container {
+      margin-right: 1rem;
       overflow: hidden;
       overflow: hidden;
       .grw-page-path-nav {
       .grw-page-path-nav {
         white-space: nowrap;
         white-space: nowrap;

+ 3 - 0
src/server/models/serializers/user-serializer.js

@@ -1,6 +1,9 @@
 function omitInsecureAttributes(user) {
 function omitInsecureAttributes(user) {
   // omit password
   // omit password
   delete user.password;
   delete user.password;
+  // omit apiToken
+  delete user.apiToken;
+
   // omit email
   // omit email
   if (!user.isEmailPublished) {
   if (!user.isEmailPublished) {
     delete user.email;
     delete user.email;

+ 3 - 2
src/server/routes/apiv3/personal-setting.js

@@ -128,10 +128,11 @@ module.exports = (crowi) => {
     try {
     try {
       const user = await User.findUserByUsername(username);
       const user = await User.findUserByUsername(username);
 
 
-      // return email whether it's private
-      const { email } = user;
+      // return email and apiToken
+      const { email, apiToken } = user;
       const currentUser = user.toObject();
       const currentUser = user.toObject();
       currentUser.email = email;
       currentUser.email = email;
+      currentUser.apiToken = apiToken;
 
 
       return res.apiv3({ currentUser });
       return res.apiv3({ currentUser });
     }
     }

+ 64 - 24
src/server/routes/apiv3/users.js

@@ -8,6 +8,7 @@ const router = express.Router();
 
 
 const { body, query } = require('express-validator');
 const { body, query } = require('express-validator');
 const { isEmail } = require('validator');
 const { isEmail } = require('validator');
+const { serializeUserSecurely } = require('../../models/serializers/user-serializer');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -88,21 +89,27 @@ module.exports = (crowi) => {
   };
   };
 
 
   validator.statusList = [
   validator.statusList = [
-    // validate status list status array match to statusNo
-    query('selectedStatusList').custom((value) => {
-      const error = [];
-      value.forEach((status) => {
-        if (!Object.keys(statusNo)) {
-          error.push(status);
-        }
-      });
-      return (error.length === 0);
+    query('selectedStatusList').if(value => value != null).custom((value, { req }) => {
+
+      const { user } = req;
+
+      if (user != null && user.admin) {
+        return value;
+      }
+      throw new Error('the param \'selectedStatusList\' is not allowed to use by the users except administrators');
     }),
     }),
     // validate sortOrder : asc or desc
     // validate sortOrder : asc or desc
     query('sortOrder').isIn(['asc', 'desc']),
     query('sortOrder').isIn(['asc', 'desc']),
     // validate sort : what column you will sort
     // validate sort : what column you will sort
     query('sort').isIn(['id', 'status', 'username', 'name', 'email', 'createdAt', 'lastLoginAt']),
     query('sort').isIn(['id', 'status', 'username', 'name', 'email', 'createdAt', 'lastLoginAt']),
     query('page').isInt({ min: 1 }),
     query('page').isInt({ min: 1 }),
+    query('forceIncludeAttributes').toArray().custom((value, { req }) => {
+      // only the admin user can specify forceIncludeAttributes
+      if (value.length === 0) {
+        return true;
+      }
+      return req.user.admin;
+    }),
   ];
   ];
 
 
   validator.recentCreatedByUser = [
   validator.recentCreatedByUser = [
@@ -156,11 +163,13 @@ module.exports = (crowi) => {
    *                      $ref: '#/components/schemas/PaginateResult'
    *                      $ref: '#/components/schemas/PaginateResult'
    */
    */
 
 
-  router.get('/', loginRequiredStrictly, validator.statusList, apiV3FormValidator, async(req, res) => {
+  router.get('/', accessTokenParser, loginRequired, validator.statusList, apiV3FormValidator, async(req, res) => {
 
 
     const page = parseInt(req.query.page) || 1;
     const page = parseInt(req.query.page) || 1;
     // status
     // status
-    const { selectedStatusList } = req.query;
+    const { forceIncludeAttributes } = req.query;
+    const selectedStatusList = req.query.selectedStatusList || ['active'];
+
     const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
     const statusNoList = (selectedStatusList.includes('all')) ? Object.values(statusNo) : selectedStatusList.map(element => statusNo[element]);
 
 
     // Search from input
     // Search from input
@@ -172,27 +181,58 @@ module.exports = (crowi) => {
       [sort]: (sortOrder === 'desc') ? -1 : 1,
       [sort]: (sortOrder === 'desc') ? -1 : 1,
     };
     };
 
 
-    try {
-      const paginateResult = await User.paginate(
+    //  For more information about the external specification of the User API, see here (https://dev.growi.org/5fd7466a31d89500488248e3)
+
+    const orConditions = [
+      { name: { $in: searchWord } },
+      { username: { $in: searchWord } },
+    ];
+
+    const query = {
+      $and: [
+        { status: { $in: statusNoList } },
         {
         {
-          $and: [
-            { status: { $in: statusNoList } },
-            {
-              $or: [
-                { name: { $in: searchWord } },
-                { username: { $in: searchWord } },
-                { email: { $in: searchWord } },
-              ],
-            },
-          ],
+          $or: orConditions,
         },
         },
+      ],
+    };
+
+    try {
+      if (req.user != null) {
+        orConditions.push(
+          {
+            $and: [
+              { isEmailPublished: true },
+              { email: { $in: searchWord } },
+            ],
+          },
+        );
+      }
+      if (forceIncludeAttributes.includes('email')) {
+        orConditions.push({ email: { $in: searchWord } });
+      }
+
+      const paginateResult = await User.paginate(
+        query,
         {
         {
           sort: sortOutput,
           sort: sortOutput,
           page,
           page,
           limit: PAGE_ITEMS,
           limit: PAGE_ITEMS,
-          select: User.USER_PUBLIC_FIELDS,
         },
         },
       );
       );
+
+      paginateResult.docs = paginateResult.docs.map((doc) => {
+
+        // return email only when specified by query
+        const { email } = doc;
+        const user = serializeUserSecurely(doc);
+        if (forceIncludeAttributes.includes('email')) {
+          user.email = email;
+        }
+
+        return user;
+      });
+
       return res.apiv3({ paginateResult });
       return res.apiv3({ paginateResult });
     }
     }
     catch (err) {
     catch (err) {