2
0
Эх сурвалжийг харах

Merge branch 'feat/tag-list-page-for-master-merge' into add-TagLabel

itizawa 7 жил өмнө
parent
commit
0fa9077b1b

+ 21 - 1
CHANGES.md

@@ -1,15 +1,35 @@
 # CHANGES
 
-## 3.4.3-RC
+## 3.4.5-RC
+
+* Improvement: Pass autolink through the XSS filter according to CommonMark Spec
+* Fix: Update ElasticSearch index when deleting/duplicating pages
+* Fix: Xss filter breaks PlantUML arrows
+* Support: Support growi-plugin-lsx@2.2.0
+* Support: Upgrade libs
+    * growi-commons
+    * xss
+
+## 3.4.4
+
+* Fix: Comment component doesn't work
+
+## 3.4.3
 
 * Improvement: Add 'antarctic' theme
 * Support Apply eslint-config-airbnb based rules
 * Support Apply prettier and stylelint
 * Support: Upgrade libs
+    * csrf
+    * escape-string-regexp
     * eslint
+    * express-session
     * googleapis
+    * growi-commons
     * i18next
+    * mini-css-extract-plugin
     * nodemailer
+    * penpal
     * react-i18next
     * string-width
 

+ 8 - 8
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.4.3-RC",
+  "version": "3.4.5-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -73,22 +73,22 @@
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
     "cross-env": "^5.0.5",
-    "csrf": "~3.0.3",
+    "csrf": "^3.1.0",
     "diff": "^4.0.1",
     "elasticsearch": "^15.0.0",
     "entities": "^1.1.1",
     "env-cmd": "^8.0.1",
     "esa-nodejs": "^0.0.7",
-    "escape-string-regexp": "^1.0.5",
+    "escape-string-regexp": "^2.0.0",
     "express": "^4.16.1",
     "express-bunyan-logger": "^1.3.3",
     "express-form": "~0.12.0",
     "express-sanitizer": "^1.0.4",
-    "express-session": "~1.15.0",
+    "express-session": "^1.16.1",
     "express-webpack-assets": "^0.1.0",
     "googleapis": "^39.1.0",
     "graceful-fs": "^4.1.11",
-    "growi-commons": "^3.1.0",
+    "growi-commons": "^4.0.1",
     "helmet": "^3.13.0",
     "i18next": "^15.0.9",
     "i18next-express-middleware": "^1.4.1",
@@ -124,7 +124,7 @@
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
     "url-join": "^4.0.0",
-    "xss": "^1.0.3"
+    "xss": "^1.0.6"
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.0.16",
@@ -179,7 +179,7 @@
     "markdown-it-toc-and-anchor-with-slugid": "^1.1.4",
     "markdown-table": "^1.1.1",
     "metismenu": "^3.0.3",
-    "mini-css-extract-plugin": "^0.5.0",
+    "mini-css-extract-plugin": "^0.6.0",
     "mocha": "^6.0.1",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
@@ -189,7 +189,7 @@
     "null-loader": "^0.1.1",
     "on-headers": "^1.0.1",
     "optimize-css-assets-webpack-plugin": "^5.0.0",
-    "penpal": "^3.0.3",
+    "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
     "prettier-stylelint": "^0.4.2",

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
public/images/themes/antarctic/bg.svg


+ 15 - 10
src/client/js/app.js

@@ -17,6 +17,7 @@ import GrowiRenderer from './util/GrowiRenderer';
 
 import HeaderSearchBox from './components/HeaderSearchBox';
 import SearchPage from './components/SearchPage';
+import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
 // eslint-disable-next-line import/no-duplicates
 import OptionsSelector from './components/PageEditor/OptionsSelector';
@@ -297,6 +298,8 @@ const componentMappings = {
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
+  'tags-page': <TagsList crowi={crowi} />,
+
   'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addTrailingSlash />,
   'rename-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
   'duplicate-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
@@ -469,16 +472,18 @@ if (writeCommentElem) {
     }
   };
   ReactDOM.render(
-    <CommentForm
-      crowi={crowi}
-      crowiOriginRenderer={crowiRenderer}
-      pageId={pageId}
-      pagePath={pagePath}
-      revisionId={pageRevisionId}
-      onPostComplete={postCompleteHandler}
-      editorOptions={editorOptions}
-      slackChannels={slackChannels}
-    />,
+    <I18nextProvider i18n={i18n}>
+      <CommentForm
+        crowi={crowi}
+        crowiOriginRenderer={crowiRenderer}
+        pageId={pageId}
+        pagePath={pagePath}
+        revisionId={pageRevisionId}
+        onPostComplete={postCompleteHandler}
+        editorOptions={editorOptions}
+        slackChannels={slackChannels}
+      />
+    </I18nextProvider>,
     writeCommentElem,
   );
 }

+ 9 - 6
src/client/js/components/PageEditorByHackmd/HackmdEditor.jsx

@@ -1,8 +1,9 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import Penpal from 'penpal';
-// Penpal.debug = true;
+import connectToChild from 'penpal/lib/connectToChild';
+
+const DEBUG_PENPAL = false;
 
 export default class HackmdEditor extends React.PureComponent {
 
@@ -28,11 +29,12 @@ export default class HackmdEditor extends React.PureComponent {
   initHackmdWithPenpal() {
     const _this = this; // for in methods scope
 
-    const url = `${this.props.hackmdUri}/${this.props.pageIdOnHackmd}?both`;
+    const iframe = document.createElement('iframe');
+    iframe.src = `${this.props.hackmdUri}/${this.props.pageIdOnHackmd}?both`;
+    this.iframeContainer.appendChild(iframe);
 
-    const connection = Penpal.connectToChild({
-      url,
-      appendTo: this.iframeContainer,
+    const connection = connectToChild({
+      iframe,
       methods: { // expose methods to HackMD
         notifyBodyChanges(document) {
           _this.notifyBodyChangesHandler(document);
@@ -41,6 +43,7 @@ export default class HackmdEditor extends React.PureComponent {
           _this.saveWithShortcutHandler(document);
         },
       },
+      debug: DEBUG_PENPAL,
     });
     connection.promise.then((child) => {
       this.hackmd = child;

+ 185 - 0
src/client/js/components/TagsList.jsx

@@ -0,0 +1,185 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Pagination from 'react-bootstrap/lib/Pagination';
+
+export default class TagsList extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      tagData: [],
+      activePage: 1,
+      paginationNumbers: {},
+    };
+
+    this.calculatePagination = this.calculatePagination.bind(this);
+  }
+
+  async componentWillMount() {
+    await this.getTagList(1);
+  }
+
+  async getTagList(selectPageNumber) {
+    const limit = 10;
+    const offset = (selectPageNumber - 1) * limit;
+    const res = await this.props.crowi.apiGet('/tags.list', { limit, offset });
+
+    const totalCount = res.totalCount;
+    const tagData = res.data;
+    const activePage = selectPageNumber;
+    const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
+
+    this.setState({
+      tagData,
+      activePage,
+      paginationNumbers,
+    });
+  }
+
+  calculatePagination(limit, totalCount, activePage) {
+    // calc totalPageNumber
+    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
+
+    let paginationStart = activePage - 2;
+    let maxViewPageNum = activePage + 2;
+    // pagination Number area size = 5 , pageNumber calculate in here
+    // activePage Position calculate ex. 4 5 [6] 7 8 (Page8 over is Max), 3 4 5 [6] 7 (Page7 is Max)
+    if (paginationStart < 1) {
+      const diff = 1 - paginationStart;
+      paginationStart += diff;
+      maxViewPageNum = Math.min(totalPage, maxViewPageNum + diff);
+    }
+    if (maxViewPageNum > totalPage) {
+      const diff = maxViewPageNum - totalPage;
+      maxViewPageNum -= diff;
+      paginationStart = Math.max(1, paginationStart - diff);
+    }
+
+    return {
+      totalPage,
+      paginationStart,
+      maxViewPageNum,
+    };
+  }
+
+  /**
+   * generate Elements of Tag
+   *
+   * @param {any} pages Array of pages Model Obj
+   *
+   */
+  generateTagList(tagData) {
+    return tagData.map((data) => {
+      return (
+        <a key={data.name} href={`/_search?q=tag:${data.name}`} className="list-group-item">
+          <p className="float-left my-0">{data.name}</p>
+        </a>
+      );
+    });
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set << & <
+   */
+  generateFirstPrev(activePage) {
+    const paginationItems = [];
+    if (activePage !== 1) {
+      paginationItems.push(
+        <Pagination.First key="first" onClick={() => { return this.getTagList(1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" onClick={() => { return this.getTagList(this.state.activePage - 1) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.First key="first" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Prev key="prev" disabled />,
+      );
+
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
+   * this function set  numbers
+   */
+  generatePaginations(activePage, paginationStart, maxViewPageNum) {
+    const paginationItems = [];
+    for (let number = paginationStart; number <= maxViewPageNum; number++) {
+      paginationItems.push(
+        <Pagination.Item key={number} active={number === activePage} onClick={() => { return this.getTagList(number) }}>{number}</Pagination.Item>,
+      );
+    }
+    return paginationItems;
+  }
+
+  /**
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set > & >>
+   */
+  generateNextLast(activePage, totalPage) {
+    const paginationItems = [];
+    if (totalPage !== activePage) {
+      paginationItems.push(
+        <Pagination.Next key="next" onClick={() => { return this.getTagList(this.state.activePage + 1) }} />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" onClick={() => { return this.getTagList(totalPage) }} />,
+      );
+    }
+    else {
+      paginationItems.push(
+        <Pagination.Next key="next" disabled />,
+      );
+      paginationItems.push(
+        <Pagination.Last key="last" disabled />,
+      );
+
+    }
+    return paginationItems;
+  }
+
+  render() {
+    const tagList = this.generateTagList(this.state.tagData);
+
+    const paginationItems = [];
+
+    const activePage = this.state.activePage;
+    const totalPage = this.state.paginationNumbers.totalPage;
+    const paginationStart = this.state.paginationNumbers.paginationStart;
+    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
+    const firstPrevItems = this.generateFirstPrev(activePage);
+    paginationItems.push(firstPrevItems);
+    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
+    paginationItems.push(paginations);
+    const nextLastItems = this.generateNextLast(activePage, totalPage);
+    paginationItems.push(nextLastItems);
+
+    return (
+      <div>
+        <ul className="list-group mx-4">{tagList}</ul>
+        <div className="text-center">
+          <Pagination>{paginationItems}</Pagination>
+        </div>
+      </div>
+    );
+  }
+
+}
+
+TagsList.propTypes = {
+  crowi: PropTypes.object.isRequired,
+};
+
+TagsList.defaultProps = {
+};

+ 5 - 4
src/client/js/hackmd-agent.js

@@ -9,11 +9,11 @@
  *
  * @author Yuki Takei <yuki@weseek.co.jp>
  */
-import Penpal from 'penpal';
-// Penpal.debug = true;
-
+import connectToParent from 'penpal/lib/connectToParent';
 import { debounce } from 'throttle-debounce';
 
+const DEBUG_PENPAL = false;
+
 /* eslint-disable no-console  */
 
 const allowedOrigin = '{{origin}}'; // will be replaced by swig
@@ -105,7 +105,7 @@ function addEventListenersToCodemirror() {
 }
 
 function connectToParentWithPenpal() {
-  const connection = Penpal.connectToParent({
+  const connection = connectToParent({
     parentOrigin: allowedOrigin,
     // Methods child is exposing to parent
     methods: {
@@ -119,6 +119,7 @@ function connectToParentWithPenpal() {
         setValueToCodemirrorOnInit(newValue);
       },
     },
+    debug: DEBUG_PENPAL,
   });
   connection.promise
     .then((parent) => {

+ 12 - 5
src/client/js/legacy/crowi.js

@@ -357,13 +357,12 @@ $(() => {
     $(this).serializeArray().forEach((obj) => {
       nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is renamed page path
     });
-
-    const data = `${$(this).serialize()}&socketClientId=${crowi.getSocketClientId()}`;
+    nameValueMap.socketClientId = crowi.getSocketClientId();
 
     $.ajax({
       type: 'POST',
       url: '/_api/pages.rename',
-      data,
+      data: nameValueMap,
       dataType: 'json',
     })
       .done((res) => {
@@ -396,11 +395,12 @@ $(() => {
     $(this).serializeArray().forEach((obj) => {
       nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is duplicated page path
     });
+    nameValueMap.socketClientId = crowi.getSocketClientId();
 
     $.ajax({
       type: 'POST',
       url: '/_api/pages.duplicate',
-      data: $(this).serialize(),
+      data: nameValueMap,
       dataType: 'json',
     }).done((res) => {
       // error
@@ -426,10 +426,17 @@ $(() => {
     $('#deletePage .msg').hide();
   });
   $('#delete-page-form').submit((e) => {
+    // create name-value map
+    const nameValueMap = {};
+    $('#delete-page-form').serializeArray().forEach((obj) => {
+      nameValueMap[obj.name] = obj.value;
+    });
+    nameValueMap.socketClientId = crowi.getSocketClientId();
+
     $.ajax({
       type: 'POST',
       url: '/_api/pages.remove',
-      data: $('#delete-page-form').serialize(),
+      data: nameValueMap,
       dataType: 'json',
     }).done((res) => {
       // error

+ 1 - 1
src/client/js/legacy/thirdparty-js/agile-admin.js

@@ -21,7 +21,7 @@ loadSettings();
 //collapses the sidebar on window resize.
 // Sets the min-height of #page-wrapper to window size
 $(window).on("load resize", function () {
-    let topOffset = 60;
+    let topOffset = $('.navbar-header').outerHeight();
     const width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
     if (width < 768) {
         $('div.navbar-collapse').addClass('collapse');

+ 8 - 2
src/client/styles/agile-admin/inverse/colors/antarctic.scss

@@ -28,7 +28,8 @@ $info:$subthemecolor;
 @import 'apply-colors-light';
 
 // change color of highlighted header in wiki (default: orange)
-.code-line {
+.code-line,
+ul>.text-muted {
   color: $subthemecolor;
 }
 
@@ -62,7 +63,8 @@ $info:$subthemecolor;
 }
 
 table,
-.hljs {
+.hljs,
+.help-block {
   background-color: $background-color;
 }
 
@@ -79,6 +81,10 @@ table,
   border-top: none;
 }
 
+.help-block {
+  padding: 5px;
+}
+
 /*
  * Accentcolor (yellow)
  */

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 390 - 197
src/client/styles/agile-admin/inverse/eliteadmin.scss


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

@@ -74,7 +74,6 @@
     h1.title {
       margin-top: 0;
       margin-bottom: 0;
-      float: left;
 
       .d-flex {
         flex-wrap: wrap; // for long page path

+ 20 - 4
src/client/styles/scss/_search.scss

@@ -19,6 +19,7 @@
 
 .search-typeahead {
   position: relative;
+
   .search-clear {
     position: absolute;
     top: 4px;
@@ -29,6 +30,7 @@
     padding: 0;
     color: #999;
   }
+
   .rbt-menu {
     margin-top: 3px;
 
@@ -38,10 +40,12 @@
         padding: 0 4px;
         color: inherit;
       }
+
       .page-list-meta {
         font-size: 0.9em;
         color: #999;
-        > span {
+
+        >span {
           margin-right: 0.3rem;
         }
       }
@@ -66,6 +70,7 @@
     border-top-left-radius: 40px;
     border-bottom-left-radius: 40px;
   }
+
   // using react-bootstrap-typeahead
   // see: https://github.com/ericgio/react-bootstrap-typeahead
   .rbt-input.form-control {
@@ -78,13 +83,16 @@
       margin-left: 8px;
     }
   }
+
   .btn-group-submit-search {
     position: absolute;
     top: 0;
     right: 0;
+
     .btn {
       padding: 4px 10px;
     }
+
     z-index: 3;
   }
 }
@@ -97,6 +105,7 @@
   .rbt-input.form-control {
     width: 200px;
     transition: 0.3s ease-out;
+
     // focus
     &.focus {
       width: 300px;
@@ -105,12 +114,14 @@
 }
 
 .search-sidebar {
+
   .search-form,
   .form-group,
   .rbt-input.form-control,
   .input-group {
     width: 100%;
   }
+
   .btn-group-submit-search {
     right: 30px;
   }
@@ -135,6 +146,7 @@
   .search-result-list {
     nav {
       padding-right: 0;
+
       &.affix {
         top: 58px;
         width: 33%;
@@ -143,9 +155,11 @@
         padding-bottom: 50px;
         overflow-y: scroll;
       }
+
       .nav {
-        > li {
+        >li {
           padding: 2px 8px;
+
           &.active {
             padding-right: 5px;
             border-right: solid 3px transparent;
@@ -173,11 +187,12 @@
       // adjust for anchor links by the height of fixed .search-page-input
       margin-top: -48px;
 
-      > h2 {
+      >h2 {
         font-size: 20px;
         line-height: 1em;
       }
-      &:first-child > h2 {
+
+      &:first-child>h2 {
         margin-top: 0;
       }
 
@@ -196,6 +211,7 @@
   top: 0;
   z-index: 99;
   padding: 10px 0;
+
   .input-group-btn .btn {
     height: 34px;
     padding: 0px 10px;

+ 42 - 0
src/lib/service/xss/commonmark-spec.js

@@ -0,0 +1,42 @@
+/**
+ * Valid schemes
+ * @see https://spec.commonmark.org/0.16/#autolinks
+ */
+const schemesForAutolink = [
+  'coap', 'doi', 'javascript', 'aaa', 'aaas', 'about', 'acap', 'cap', 'cid', 'crid', 'data', 'dav', 'dict', 'dns',
+  'file', 'ftp', 'geo', 'go', 'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap', 'info', 'ipp', 'iris',
+  'iris.beep', 'iris.xpc', 'iris.xpcs', 'iris.lwz', 'ldap', 'mailto', 'mid', 'msrp', 'msrps', 'mtqp', 'mupdate',
+  'news', 'nfs', 'ni', 'nih', 'nntp', 'opaquelocktoken', 'pop', 'pres', 'rtsp', 'service', 'session', 'shttp',
+  'sieve', 'sip', 'sips', 'sms', 'snmp,soap.beep', 'soap.beeps', 'tag', 'tel', 'telnet', 'tftp', 'thismessage',
+  'tn3270', 'tip', 'tv', 'urn', 'vemmi', 'ws', 'wss', 'xcon', 'xcon-userid', 'xmlrpc.beep', 'xmlrpc.beeps', 'xmpp',
+  'z39.50r', 'z39.50s', 'adiumxtra', 'afp', 'afs', 'aim', 'apt,attachment', 'aw', 'beshare', 'bitcoin', 'bolo',
+  'callto', 'chrome,chrome-extension', 'com-eventbrite-attendee', 'content', 'cvs,dlna-playsingle', 'dlna-playcontainer',
+  'dtn', 'dvb', 'ed2k', 'facetime', 'feed', 'finger', 'fish', 'gg', 'git', 'gizmoproject', 'gtalk', 'hcp', 'icon',
+  'ipn', 'irc', 'irc6', 'ircs', 'itms', 'jar', 'jms', 'keyparc', 'lastfm', 'ldaps', 'magnet', 'maps', 'market,message',
+  'mms', 'ms-help', 'msnim', 'mumble', 'mvn', 'notes', 'oid', 'palm', 'paparazzi', 'platform', 'proxy', 'psyc',
+  'query', 'res', 'resource', 'rmi', 'rsync', 'rtmp', 'secondlife', 'sftp', 'sgn', 'skype', 'smb', 'soldat', 'spotify',
+  'ssh', 'steam', 'svn', 'teamspeak', 'things', 'udp', 'unreal', 'ut2004', 'ventrilo', 'view-source', 'webcal',
+  'wtai', 'wyciwyg', 'xfire', 'xri', 'ymsgr',
+];
+const schemesCondition = schemesForAutolink.join('|');
+
+/**
+ * RegExp for URI
+ * @type {RegExp}
+ * @see https://spec.commonmark.org/0.16/#autolinks
+ */
+const uriAutolinkRegexp = new RegExp(`^(${schemesCondition}):\\/\\/.+$`);
+
+/**
+ * RegExp for email
+ * @type {RegExp}
+ * @see https://spec.commonmark.org/0.16/#autolinks
+ */
+// eslint-disable-next-line max-len
+const emailAutolinkRegexp = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
+
+
+module.exports = {
+  uriAutolinkRegexp,
+  emailAutolinkRegexp,
+};

+ 9 - 1
src/lib/service/xss/index.js

@@ -1,7 +1,9 @@
+const xss = require('xss');
+const commonmarkSpec = require('./commonmark-spec');
+
 class Xss {
 
   constructor(xssOption) {
-    const xss = require('xss');
 
     xssOption = xssOption || {}; // eslint-disable-line no-param-reassign
 
@@ -17,6 +19,12 @@ class Xss {
       css: false,
       whiteList: whiteListContent,
       escapeHtml: (html) => { return html }, // resolve https://github.com/weseek/growi/issues/221
+      onTag: (tag, html, options) => {
+        // pass autolink
+        if (tag.match(commonmarkSpec.uriAutolinkRegexp) || tag.match(commonmarkSpec.emailAutolinkRegexp)) {
+          return html;
+        }
+      },
     };
 
     tagWhiteList.forEach((tag) => {

+ 1 - 1
src/lib/service/xss/recommendedXssWhiteList.js → src/lib/service/xss/recommended-whitelist.js

@@ -5,7 +5,7 @@
  */
 
 const tags = [
-  'a', 'b', 'blockquote', 'blockquote', 'code', 'del', 'dd', 'dl', 'dt', 'em',
+  '-', 'a', 'b', 'blockquote', 'blockquote', 'code', 'del', 'dd', 'dl', 'dt', 'em',
   'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'img', 'kbd', 'li', 'ol', 'p', 'pre',
   's', 'sup', 'sub', 'strong', 'strike', 'ul', 'br', 'hr', 'span', 'div', 'iframe',
   'table', 'thead', 'tbody', 'tfoot', 'th', 'td', 'tr', 'colgroup', 'col',

+ 3 - 3
src/lib/service/xss/xssOption.js

@@ -1,12 +1,12 @@
 class XssOption {
 
   constructor(config) {
-    const recommendedXssWhiteList = require('./recommendedXssWhiteList');
+    const recommendedWhitelist = require('./recommended-whitelist');
     const initializedConfig = (config != null) ? config : {};
 
     this.isEnabledXssPrevention = initializedConfig.isEnabledXssPrevention || true;
-    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedXssWhiteList.tags;
-    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedXssWhiteList.attrs;
+    this.tagWhiteList = initializedConfig.tagWhiteList || recommendedWhitelist.tags;
+    this.attrWhiteList = initializedConfig.attrWhiteList || recommendedWhitelist.attrs;
   }
 
 }

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

@@ -7,7 +7,7 @@ module.exports = function(crowi) {
   const mongoose = require('mongoose');
   const debug = require('debug')('growi:models:config');
   const uglifycss = require('uglifycss');
-  const recommendedXssWhiteList = require('@commons/service/xss/recommendedXssWhiteList');
+  const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
 
   const SECURITY_RESTRICT_GUEST_MODE_DENY = 'Deny';
   const SECURITY_RESTRICT_GUEST_MODE_READONLY = 'Readonly';
@@ -430,7 +430,7 @@ module.exports = function(crowi) {
           return [];
 
         case 2: // recommended
-          return recommendedXssWhiteList.tags;
+          return recommendedWhitelist.tags;
 
         case 3: // custom white list
           return config.markdown[key];
@@ -453,7 +453,7 @@ module.exports = function(crowi) {
           return [];
 
         case 2: // recommended
-          return recommendedXssWhiteList.attrs;
+          return recommendedWhitelist.attrs;
 
         case 3: // custom white list
           return config.markdown[key];

+ 15 - 0
src/server/models/page-tag-relation.js

@@ -37,6 +37,21 @@ class PageTagRelation {
     }
   }
 
+  static async createTagListWithCount(option) {
+    const opt = option || {};
+    const sortOpt = opt.sortOpt || {};
+    const offset = opt.offset || 0;
+    const limit = opt.limit || 50;
+
+    const list = await this.aggregate()
+      .group({ _id: '$relatedTag', count: { $sum: 1 } })
+      .sort(sortOpt)
+      .skip(offset)
+      .limit(limit);
+
+    return list;
+  }
+
 }
 
 module.exports = function() {

+ 2 - 5
src/server/models/page.js

@@ -573,7 +573,7 @@ module.exports = function(crowi) {
       /\s+\/\s+/, // avoid miss in renaming
       /.+\/edit$/,
       /.+\.md$/,
-      /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments)(\/.*|$)/,
+      /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags)(\/.*|$)/,
     ];
 
     let isCreatable = true;
@@ -1098,11 +1098,8 @@ module.exports = function(crowi) {
       }
 
       pageData.status = STATUS_DELETED;
-      const updatedPageData = await this.rename(pageData, newPath, user, { createRedirectPage: true });
+      const updatedPageData = await this.rename(pageData, newPath, user, { socketClientId, createRedirectPage: true });
 
-      if (socketClientId != null) {
-        pageEvent.emit('delete', updatedPageData, user, socketClientId);
-      }
       return updatedPageData;
     }
 

+ 2 - 1
src/server/models/tag.js

@@ -32,7 +32,8 @@ class Tag {
 
 }
 
-module.exports = function() {
+module.exports = function(crowi) {
+  Tag.crowi = crowi;
   schema.loadClass(Tag);
   const model = mongoose.model('Tag', schema);
   return model;

+ 2 - 2
src/server/routes/admin.js

@@ -14,7 +14,7 @@ module.exports = function(crowi, app) {
   const GlobalNotificationMailSetting = models.GlobalNotificationMailSetting;
   const GlobalNotificationSlackSetting = models.GlobalNotificationSlackSetting; // eslint-disable-line no-unused-vars
 
-  const recommendedXssWhiteList = require('@commons/service/xss/recommendedXssWhiteList');
+  const recommendedWhitelist = require('@commons/service/xss/recommended-whitelist');
   const PluginUtils = require('../plugins/plugin-utils');
   const ApiResponse = require('../util/apiResponse');
   const importer = require('../util/importer')(crowi);
@@ -117,7 +117,7 @@ module.exports = function(crowi, app) {
 
     return res.render('admin/markdown', {
       markdownSetting,
-      recommendedXssWhiteList,
+      recommendedWhitelist,
     });
   };
 

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

@@ -202,6 +202,8 @@ module.exports = function(crowi, app) {
   app.post('/_api/pages.revertRemove' , loginRequired(crowi, app) , csrf, page.api.revertRemove); // (Avoid from API Token)
   app.post('/_api/pages.unlink'       , loginRequired(crowi, app) , csrf, page.api.unlink); // (Avoid from API Token)
   app.post('/_api/pages.duplicate', accessTokenParser, loginRequired(crowi, app), csrf, page.api.duplicate);
+  app.get('/tags'                     , loginRequired(crowi, app, false), tag.showPage);
+  app.get('/_api/tags.list'           , accessTokenParser, loginRequired(crowi, app, false), tag.api.list);
   app.get('/_api/tags.search'         , accessTokenParser, loginRequired(crowi, app, false), tag.api.search);
   app.get('/_api/comments.get'        , accessTokenParser , loginRequired(crowi, app, false) , comment.api.get);
   app.post('/_api/comments.add'       , form.comment, accessTokenParser , loginRequired(crowi, app) , csrf, comment.api.add);

+ 48 - 0
src/server/routes/tag.js

@@ -1,12 +1,16 @@
 module.exports = function(crowi, app) {
 
   const Tag = crowi.model('Tag');
+  const PageTagRelation = crowi.model('PageTagRelation');
   const ApiResponse = require('../util/apiResponse');
   const actions = {};
   const api = {};
 
   actions.api = api;
 
+  actions.showPage = function(req, res) {
+    return res.render('tags');
+  };
 
   /**
    * @api {get} /tags.search search tags
@@ -21,5 +25,49 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success({ tags }));
   };
 
+  /**
+   * @api {get} /tags.list get tagnames and count pages relate each tag
+   * @apiName tagList
+   * @apiGroup Tag
+   *
+   * @apiParam {Number} limit
+   * @apiParam {Number} offset
+   */
+  api.list = async function(req, res) {
+    const limit = +req.query.limit || 50;
+    const offset = +req.query.offset || 0;
+    const sortOpt = { count: -1 };
+    const queryOptions = { offset, limit, sortOpt };
+    const result = {};
+
+    try {
+      // get tag list contains id and count properties
+      const list = await PageTagRelation.createTagListWithCount(queryOptions);
+      const ids = list.map((obj) => { return obj._id });
+
+      // get tag documents for add name data to the list
+      const tags = await Tag.find({ _id: { $in: ids } });
+
+      // add name property
+      result.data = list.map((elm) => {
+        const data = {};
+        const tag = tags.find((tag) => { return (tag.id === elm._id.toString()) });
+
+        data._id = elm._id;
+        data.name = tag.name;
+        data.count = elm.count; // the number of related pages
+        return data;
+      });
+
+      result.totalCount = await Tag.count();
+
+      return res.json(ApiResponse.success(result));
+    }
+    catch (err) {
+      return res.json(ApiResponse.error(err));
+    }
+  };
+
+
   return actions;
 };

+ 2 - 2
src/server/views/admin/markdown.html

@@ -193,11 +193,11 @@
               <p class="font-weight-bold">{{ t('markdown_setting.Recommended setting') }}</p>
               <div class="m-t-15">
                 {{ t('markdown_setting.Tag names') }}
-                <textarea class="form-control xss-list" name="recommendedTags" rows="6" cols="40" readonly>{{ recommendedXssWhiteList.tags }}</textarea>
+                <textarea class="form-control xss-list" name="recommendedTags" rows="6" cols="40" readonly>{{ recommendedWhitelist.tags }}</textarea>
               </div>
               <div class="m-t-15">
                 {{ t('markdown_setting.Tag attributes') }}
-                <textarea class="form-control xss-list" name="recommendedAttrs" rows="6" cols="40" readonly>{{ recommendedXssWhiteList.attrs }}</textarea>
+                <textarea class="form-control xss-list" name="recommendedAttrs" rows="6" cols="40" readonly>{{ recommendedWhitelist.attrs }}</textarea>
               </div>
             </label>
           </div>

+ 2 - 0
src/server/views/layout-growi/widget/header.html

@@ -8,9 +8,11 @@
       </div>
       <div class="title-container">
         <h1 class="title" id="revision-path"></h1>
+
         <!-- [TODO] commentout Until the destination is decided -->
         <!-- <div id="revision-url" class="url-line"></div> -->
         <div class="title" id="tag-label"></div>
+
       </div>
       {% if page %}
       {% include '../../widget/header-buttons.html' %}

+ 5 - 0
src/server/views/layout/layout.html

@@ -152,6 +152,11 @@
             <i class="ti-menu"></i>
           </a>
         </li>
+        <li>
+          <a href="/tags">
+            <i class="fa fa-tags"></i>Tags
+          </a>
+        </li>
         <li>
           {% if searchConfigured() %}
           <div class="navbar-form navbar-left search-top" role="search" id="search-top"></div>

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

@@ -0,0 +1,26 @@
+{% extends 'layout/layout.html' %}
+
+{% block layout_main %}
+<div class="container-fluid">
+  <div class="row bg-title hidden-print">
+    <div class="col-xs-12 header-container">
+      {% block content_header %}
+      <div class="header-wrap">
+        <header id="page-header">
+          <h1 id="admin-title" class="title">Tags</h1>
+        </header>
+      </div>
+      {% endblock %}
+    </div>
+  </div>
+  <div class="row">
+    <div id="main" class="main m-t-15 col-md-12 tags-page">
+      <div class="" id="tags-page"></div>
+    </div>
+  </div>
+</div><!-- /.container-fluid -->
+
+<footer class="footer">
+  {% include 'widget/system-version.html' %}
+</footer>
+{% endblock %} {# layout_main #}

+ 87 - 41
yarn.lock

@@ -2622,10 +2622,6 @@ cosmiconfig@^5.0.0:
     js-yaml "^3.9.0"
     parse-json "^4.0.0"
 
-crc@3.4.4:
-  version "3.4.4"
-  resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b"
-
 create-ecdh@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.0.tgz#888c723596cdf7612f6498233eebd7a35301737d"
@@ -2740,13 +2736,14 @@ crypto-random-string@^1.0.0:
   resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"
   integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=
 
-csrf@~3.0.3:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.0.6.tgz#b61120ddceeafc91e76ed5313bb5c0b2667b710a"
+csrf@^3.1.0:
+  version "3.1.0"
+  resolved "https://registry.yarnpkg.com/csrf/-/csrf-3.1.0.tgz#ec75e9656d004d674b8ef5ba47b41fbfd6cb9c30"
+  integrity sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==
   dependencies:
     rndm "1.2.0"
-    tsscmp "1.0.5"
-    uid-safe "2.1.4"
+    tsscmp "1.0.6"
+    uid-safe "2.1.5"
 
 css-color-names@^0.0.4:
   version "0.0.4"
@@ -3073,6 +3070,11 @@ depd@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
 
+depd@~2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
+  integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
+
 des.js@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
@@ -3612,6 +3614,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
 
+escape-string-regexp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
+  integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
+
 eslint-config-airbnb-base@^13.1.0:
   version "13.1.0"
   resolved "https://registry.yarnpkg.com/eslint-config-airbnb-base/-/eslint-config-airbnb-base-13.1.0.tgz#b5a1b480b80dfad16433d6c4ad84e6605052c05c"
@@ -3937,19 +3944,19 @@ express-sanitizer@^1.0.4:
     sanitizer "0.1.3"
     underscore "1.8.3"
 
-express-session@~1.15.0:
-  version "1.15.6"
-  resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.15.6.tgz#47b4160c88f42ab70fe8a508e31cbff76757ab0a"
+express-session@^1.16.1:
+  version "1.16.1"
+  resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.16.1.tgz#251ff9776c59382301de6c8c33411af357ed439c"
+  integrity sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==
   dependencies:
     cookie "0.3.1"
     cookie-signature "1.0.6"
-    crc "3.4.4"
     debug "2.6.9"
-    depd "~1.1.1"
-    on-headers "~1.0.1"
+    depd "~2.0.0"
+    on-headers "~1.0.2"
     parseurl "~1.3.2"
+    safe-buffer "5.1.2"
     uid-safe "~2.1.5"
-    utils-merge "1.0.1"
 
 express-webpack-assets@^0.1.0:
   version "0.1.0"
@@ -4819,10 +4826,10 @@ graceful-fs@^4.1.15:
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
   integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
 
-growi-commons@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-3.1.0.tgz#f75756d7c34aa2f96612243968b3e7b8c36a5280"
-  integrity sha512-x57/t8is1SIAA7NkdTCgCNAWpETNbUb7mnd6X4wbc7z5WbiqURB6GBECR7uNxLvGDFl6/6ngknOZLy+AmhB/ng==
+growi-commons@^4.0.1:
+  version "4.0.1"
+  resolved "https://registry.yarnpkg.com/growi-commons/-/growi-commons-4.0.1.tgz#e0e71c9c286f493e11c0703c809385bcdc6a97a9"
+  integrity sha512-haH4Av1WuQIHic4Jv2RRwDprbKecRKF/3C0wVk9ssBzWtB3V6Oghj5gksajDpYOd7tOKdvkVEqqkFfIV4JQUyQ==
 
 growl@1.10.5:
   version "1.10.5"
@@ -5678,7 +5685,7 @@ is-path-inside@^1.0.0:
   dependencies:
     path-is-inside "^1.0.1"
 
-is-plain-obj@^1.1.0:
+is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
   integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@@ -6772,12 +6779,13 @@ mimic-fn@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
 
-mini-css-extract-plugin@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz#ac0059b02b9692515a637115b0cc9fed3a35c7b0"
-  integrity sha512-IuaLjruM0vMKhUUT51fQdQzBYTX49dLj8w68ALEAe2A4iYNpIC4eMac67mt3NzycvjOlf07/kYxJDc0RTl1Wqw==
+mini-css-extract-plugin@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9"
+  integrity sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==
   dependencies:
     loader-utils "^1.1.0"
+    normalize-url "^2.0.1"
     schema-utils "^1.0.0"
     webpack-sources "^1.1.0"
 
@@ -7358,6 +7366,15 @@ normalize-selector@^0.2.0:
   resolved "https://registry.yarnpkg.com/normalize-selector/-/normalize-selector-0.2.0.tgz#d0b145eb691189c63a78d201dc4fdb1293ef0c03"
   integrity sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=
 
+normalize-url@^2.0.1:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6"
+  integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==
+  dependencies:
+    prepend-http "^2.0.0"
+    query-string "^5.0.1"
+    sort-keys "^2.0.0"
+
 normalize-url@^3.0.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.2.0.tgz#98d0948afc82829f374320f405fe9ca55a5f8567"
@@ -7562,6 +7579,11 @@ on-headers@^1.0.1, on-headers@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
 
+on-headers@~1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f"
+  integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==
+
 once@^1.3.0, once@^1.3.1, once@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
@@ -8001,9 +8023,10 @@ pbkdf2@^3.0.3:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
-penpal@^3.0.3:
-  version "3.0.3"
-  resolved "https://registry.yarnpkg.com/penpal/-/penpal-3.0.3.tgz#6cdbd99d8c5dadb73be16d9fe6807826b0d9a715"
+penpal@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/penpal/-/penpal-4.0.0.tgz#1cba7a64600c1e601f91dac393c21843c977bdec"
+  integrity sha512-APPdpVijTF64nEHDfr5JFCW+aYFUe8FzKQs7hqXJis8UF7fbZ06MXUIlp5qdpmb1HdSwU7ysGbsixYGlofD8mw==
 
 performance-now@^2.1.0:
   version "2.1.0"
@@ -8479,6 +8502,11 @@ prepend-http@^1.0.1:
   resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
   integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
 
+prepend-http@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
+  integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
+
 preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
@@ -8674,6 +8702,15 @@ qs@^6.5.2, qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 
+query-string@^5.0.1:
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb"
+  integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==
+  dependencies:
+    decode-uri-component "^0.2.0"
+    object-assign "^4.1.0"
+    strict-uri-encode "^1.0.0"
+
 querystring-es3@^0.2.0:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@@ -9986,6 +10023,13 @@ socket.io@^2.0.3:
     socket.io-client "2.0.4"
     socket.io-parser "~3.1.1"
 
+sort-keys@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128"
+  integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=
+  dependencies:
+    is-plain-obj "^1.0.0"
+
 source-list-map@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085"
@@ -10200,6 +10244,11 @@ streamsearch@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
 
+strict-uri-encode@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
+  integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=
+
 string-template@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
@@ -10795,9 +10844,10 @@ tslib@^1.9.0:
   version "1.9.2"
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.2.tgz#8be0cc9a1f6dc7727c38deb16c2ebd1a2892988e"
 
-tsscmp@1.0.5:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.5.tgz#7dc4a33af71581ab4337da91d85ca5427ebd9a97"
+tsscmp@1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb"
+  integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==
 
 tty-browserify@0.0.0:
   version "0.0.0"
@@ -10885,15 +10935,10 @@ uglifycss@^0.0.29:
   version "0.0.29"
   resolved "https://registry.yarnpkg.com/uglifycss/-/uglifycss-0.0.29.tgz#abe49531155d146e75dd2fdf933d371bc1180054"
 
-uid-safe@2.1.4:
-  version "2.1.4"
-  resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.4.tgz#3ad6f38368c6d4c8c75ec17623fb79aa1d071d81"
-  dependencies:
-    random-bytes "~1.0.0"
-
-uid-safe@~2.1.5:
+uid-safe@2.1.5, uid-safe@~2.1.5:
   version "2.1.5"
   resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a"
+  integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==
   dependencies:
     random-bytes "~1.0.0"
 
@@ -11556,9 +11601,10 @@ xpath@0.0.27:
   version "0.0.27"
   resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"
 
-xss@^1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.3.tgz#d04bd2558fd6c29c46113824d5e8b2a910054e23"
+xss@^1.0.6:
+  version "1.0.6"
+  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51"
+  integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A==
   dependencies:
     commander "^2.9.0"
     cssfilter "0.0.10"

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно