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

Merge pull request #449 from weseek/master

release v3.1.2
Yuki Takei 7 жил өмнө
parent
commit
d182469d3e

+ 0 - 1
.gitignore

@@ -34,4 +34,3 @@ package-lock.json
 
 # IDE #
 .idea
-.vscode

+ 33 - 0
.vscode/launch.json

@@ -0,0 +1,33 @@
+{
+    // IntelliSense を使用して利用可能な属性を学べます。
+    // 既存の属性の説明をホバーして表示します。
+    // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+      {
+        "type": "node",
+        "request": "launch",
+        "name": "Debug: Server",
+        "runtimeExecutable": "npm",
+        "runtimeArgs": [
+          "run",
+          "server:debug"
+        ],
+        "port": 9229,
+        "restart": true,
+        "console": "integratedTerminal",
+        "internalConsoleOptions": "neverOpen"
+      },
+      {
+        "type": "chrome",
+        "request": "launch",
+        "name": "Debug: Chrome",
+        "sourceMaps": true,
+        "sourceMapPathOverrides": {
+          "webpack:///*": "${workspaceRoot}/*"
+        },
+        "url": "http://localhost:3000",
+        "webRoot": "${workspaceRoot}/public"
+      }
+    ]
+}

+ 19 - 0
.vscode/tasks.json

@@ -0,0 +1,19 @@
+{
+    // See https://go.microsoft.com/fwlink/?LinkId=733558
+    // for the documentation about the tasks.json format
+    "version": "2.0.0",
+    "tasks": [
+        {
+            "type": "npm",
+            "script": "build",
+            "problemMatcher": [
+                "$eslint-compact"
+            ]
+        },
+        {
+            "type": "npm",
+            "script": "server",
+            "problemMatcher": []
+        }
+    ]
+}

+ 27 - 1
CHANGES.md

@@ -1,9 +1,35 @@
 CHANGES
 ========
 
-## 3.1.1-RC
+## 3.1.2-RC
 
+* Feature: Template page
 * Improvement: Add 'future' theme
+* Improvement: Modify syntax for Crowi compatible template feature
+    * *before*
+        ~~~
+        ``` template:/page/name
+        page contents
+        ```
+        ~~~
+    * *after*
+        ~~~
+        ::: template:/page/name
+        page contents
+        :::
+        ~~~
+* Improvement: Escape iframe tag in block codes
+* Fix: Posting to Slack doesn't work
+    * Introduced by 3.1.0
+* Support: Upgrade libs
+    * assets-webpack-plugin
+    * googleapis
+    * react-clipboard.js
+    * xss
+
+## 3.1.1
+
+* Improvement: Add 'blue-night' theme
 * Improvement: List up pages which restricted for Group ACL
 * Fix: PageGroupRelation didn't remove when page is removed completely
 

+ 24 - 2
lib/locales/en-US/translation.json

@@ -26,10 +26,11 @@
   "Sign in with Google Account": "Sign in with Google Account",
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Example": "Example",
-  "Taro Yamada": "James Bond",
+  "Taro Yamada": "John Doe",
 
   "List View": "List",
   "Timeline View": "Timeline",
+  "History": "History",
   "Presentation Mode": "Presentation",
 
   "Created": "Created",
@@ -41,6 +42,8 @@
 
   "Management Wiki": "Management Wiki",
 
+  "Create/Edit Template": "Create/Edit Template Page",
+
   "Unportalize": "Unportalize",
 
   "View this version": "View this version",
@@ -77,7 +80,7 @@
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New Page",
-  "Create under": "Create page under: <code>%s</code>",
+  "Create under": "Create page under below:",
 
   "Table of Contents": "Table of Contents",
 
@@ -219,6 +222,25 @@
               }
   },
 
+  "template": {
+    "modal_label": {
+      "Create/Edit Template Page": "Create/Edit Template Page",
+      "Create template under": "Create template page under: <code>%s</code>"
+    },
+    "option_label": {
+      "create/edit": "Create/Edit Template page..",
+      "select": "Select template page type"
+    },
+    "local": {
+      "label": "Template for children",
+      "desc": "Applies only to the same level pages which the template exists"
+    },
+    "global": {
+      "label": "Template for descendants",
+      "desc": "Applies to all decendant pages"
+    }
+  },
+
   "admin_top": {
     "Management Wiki": "Management Wiki",
     "System Information": "System Information",

+ 24 - 4
lib/locales/ja/translation.json

@@ -30,6 +30,8 @@
 
   "List View": "リスト表示",
   "Timeline View": "タイムライン表示",
+  "History": "更新履歴",
+  "Presentation Mode": "プレゼンテーション",
 
   "Created": "作成日",
   "Last updated": "最終更新",
@@ -38,6 +40,8 @@
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
 
+  "Create/Edit Template": "テンプレートページの作成/編集",
+
   "Unportalize": "ポータル解除",
 
   "View this version": "このバージョンを見る",
@@ -75,10 +79,7 @@
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",
-  "Create under": "<code>%s</code>以下に作成",
-
-
-
+  "Create under": "ページを以下に作成",
 
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
@@ -236,6 +237,25 @@
     }
   },
 
+  "template": {
+    "modal_label": {
+      "Create/Edit Template Page": "テンプレートページの作成/編集",
+      "Create template under": "<code>%s</code> にテンプレートページを作成"
+    },
+    "option_label": {
+      "select": "テンプレートタイプを選択してください",
+      "create/edit": "テンプレートページの作成/編集.."
+    },
+    "local": {
+      "label": "同一階層テンプレート",
+      "desc": "テンプレートページが存在する階層にのみ適応されます"
+    },
+    "global": {
+      "label": "下位層テンプレート",
+      "desc": "テンプレートページが存在する下位層のすべてのページに適応されます"
+    }
+  },
+
   "admin_top": {
     "Management Wiki": "Wiki管理",
     "System Information": "システム情報",

+ 130 - 25
lib/models/page.js

@@ -392,10 +392,10 @@ module.exports = function(crowi) {
 
   pageSchema.statics.isCreatableName = function(name) {
     var forbiddenPages = [
-      /\^|\$|\*|\+|\#/,
-      /^\/_.*/, // /_api/* and so on
-      /^\/\-\/.*/,
+      /\^|\$|\*|\+|#/,
+      /^\/-\/.*/,
       /^\/_r\/.*/,
+      /^\/_apix?(\/.*)?/,
       /^\/?https?:\/\/.+$/, // avoid miss in renaming
       /\/{2,}/,             // avoid miss in renaming
       /\s+\/\s+/,           // avoid miss in renaming
@@ -527,19 +527,111 @@ module.exports = function(crowi) {
     });
   };
 
-  // find page by path
-  pageSchema.statics.findPageByPath = function(path) {
-    var Page = this;
+  // check if a given page has a children and decendants tempalte
+  pageSchema.statics.checkIfTemplatesExist = function(path) {
+    const Page = this;
+    const pathList = generatePathsOnTree(path, []);
+    const regexpList = pathList.map(path => new RegExp(`${path}/_{1,2}template`));
+    let templateInfo = {
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
+    };
 
-    return new Promise(function(resolve, reject) {
-      Page.findOne({path: path}, function(err, pageData) {
-        if (err || pageData === null) {
-          return reject(err);
-        }
+    return Page
+      .find({path: {$in: regexpList}})
+      .then(templates => {
+        templateInfo.childrenTemplateExists = (assignTemplateByType(templates, path, '_') ? true : false);
+        templateInfo.decendantsTemplateExists = (assignTemplateByType(templates, path, '__') ? true : false);
 
-        return resolve(pageData);
+        return templateInfo;
       });
-    });
+  };
+
+  /**
+   * find all templates applicable to the new page
+   */
+  pageSchema.statics.findTemplate = function(path) {
+    const Page = this;
+    const templatePath = cutOffLastSlash(path);
+    const pathList = generatePathsOnTree(templatePath, []);
+    const regexpList = pathList.map(path => new RegExp(`^${path}/_{1,2}template$`));
+
+    return Page
+      .find({path: {$in: regexpList}})
+      .populate({path: 'revision', model: 'Revision'})
+      .then(templates => {
+        return fetchTemplate(templates, templatePath);
+      });
+  };
+
+  const cutOffLastSlash = path => {
+    const lastSlash = path.lastIndexOf('/');
+    return path.substr(0, lastSlash);
+  };
+
+  const generatePathsOnTree = (path, pathList) => {
+    if (path === '') {
+      return pathList;
+    }
+
+    pathList.push(path);
+    const newPath = cutOffLastSlash(path);
+
+    return generatePathsOnTree(newPath, pathList);
+  };
+
+  const assignTemplateByType = (templates, path, type) => {
+    for (let i = 0; i < templates.length; i++) {
+      if (templates[i].path === `${path}/${type}template`) {
+        return templates[i];
+      }
+    }
+  };
+
+  const assignDecendantsTemplate = (decendantsTemplates, path) => {
+    const decendantsTemplate = assignTemplateByType(decendantsTemplates, path, '__');
+    if (decendantsTemplate) {
+      return decendantsTemplate;
+    }
+
+    if (path === '') {
+      return;
+    }
+
+    const newPath = cutOffLastSlash(path);
+    return assignDecendantsTemplate(decendantsTemplates, newPath);
+  };
+
+  const fetchTemplate = (templates, templatePath) => {
+    let templateBody;
+    /**
+     * get children template
+     * __tempate: applicable only to immediate decendants
+     */
+    const childrenTemplate = assignTemplateByType(templates, templatePath, '_');
+
+    /**
+     * get decendants templates
+     * _tempate: applicable to all pages under
+     */
+    const decendantsTemplate = assignDecendantsTemplate(templates, templatePath);
+
+    if (childrenTemplate) {
+      templateBody =  childrenTemplate.revision.body;
+    }
+    else if (decendantsTemplate) {
+      templateBody = decendantsTemplate.revision.body;
+    }
+
+    return templateBody;
+  };
+
+  // find page by path
+  pageSchema.statics.findPageByPath = function(path) {
+    if (path == null) {
+      return null;
+    }
+    return this.findOne({path});
   };
 
   pageSchema.statics.findListByPageIds = function(ids, options) {
@@ -918,6 +1010,7 @@ module.exports = function(crowi) {
       grant = GRANT_PUBLIC;
     }
 
+    let savedPage = undefined;
     return Page.findOne({path: path})
       .then(pageData => {
         if (pageData) {
@@ -939,14 +1032,18 @@ module.exports = function(crowi) {
         return newPage.save();
       })
       .then((newPage) => {
-        const newRevision = Revision.prepareRevision(newPage, body, user, {format: format});
-        return Page.pushRevision(newPage, newRevision, user)
-          .then(() => {
-            return Page.updateGrantUserGroup(newPage, grant, grantUserGroupId, user);
-          });
+        savedPage = newPage;
       })
-      .then((data) => {
-        pageEvent.emit('create', data, user);
+      .then(() => {
+        const newRevision = Revision.prepareRevision(savedPage, body, user, {format: format});
+        return Page.pushRevision(savedPage, newRevision, user);
+      })
+      .then(() => {
+        return Page.updateGrantUserGroup(savedPage, grant, grantUserGroupId, user);
+      })
+      .then(() => {
+        pageEvent.emit('create', savedPage, user);
+        return savedPage;
       });
   };
 
@@ -959,14 +1056,22 @@ module.exports = function(crowi) {
     // update existing page
     var newRevision = Revision.prepareRevision(pageData, body, user);
 
+    let savedPage = undefined;
     return Page.pushRevision(pageData, newRevision, user)
-      .then(function(revision) {
-        return Page.updateGrant(pageData, grant, user, grantUserGroupId);
+      .then((revision) => {
+        // fetch Page
+        return Page.findPageByPath(revision.path).populate('revision');
       })
-      .then(function(data) {
+      .then((page) => {
+        savedPage = page;
+      })
+      .then(() => {
+        return Page.updateGrant(savedPage, grant, user, grantUserGroupId);
+      })
+      .then((data) => {
         debug('Page grant update:', data);
-        pageEvent.emit('update', data, user);
-        return data;
+        pageEvent.emit('update', savedPage, user);
+        return savedPage;
       });
   };
 

+ 36 - 13
lib/routes/page.js

@@ -251,11 +251,15 @@ module.exports = function(crowi, app) {
       pages: [],
       tree: [],
       pageRelatedGroup: null,
+      template: null,
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
     };
 
     var pageTeamplate = 'customlayout-selector/page';
 
     var isRedirect = false;
+    var originalPath = path;
     Page.findPage(path, req.user, req.query.revision)
     .then(function(page) {
       debug('Page found', page._id, page.path);
@@ -278,6 +282,13 @@ module.exports = function(crowi, app) {
         .then(function(tree) {
           renderVars.tree = tree;
         })
+        .then(function() {
+          return Page.checkIfTemplatesExist(originalPath);
+        })
+        .then(function(templateInfo) {
+          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
+          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
+        })
         .then(() => {
           return PageGroupRelation.findByPage(renderVars.page);
         })
@@ -318,11 +329,14 @@ module.exports = function(crowi, app) {
         });
       }
     })
-    // page not exists
+    // look for templates if page not exists
     .catch(function(err) {
-      debug('Page not found', path);
-      // change template
       pageTeamplate = 'customlayout-selector/not_found';
+
+      return Page.findTemplate(originalPath)
+        .then(template => {
+          renderVars.template = template;
+        });
     })
     // get list pages
     .then(function() {
@@ -429,12 +443,17 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      return res.render('customlayout-selector/not_found', {
-        author: {},
-        page: false,
-      });
+      return Page.findTemplate(getPathFromRequest(req))
+        .then((template) => {
+          return res.render('customlayout-selector/not_found', {
+            author: {},
+            page: false,
+            template,
+          });
+        });
     }
 
+
     if (pageData.redirectTo) {
       return res.redirect(encodeURI(pageData.redirectTo + '?redirectFrom=' + pagePathUtil.encodePagePath(pageData.path)));
     }
@@ -444,6 +463,8 @@ module.exports = function(crowi, app) {
       page: pageData,
       revision: pageData.revision || {},
       author: pageData.revision.author || false,
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
     };
     var userPage = isUserPage(pageData.path);
     var userData = null;
@@ -488,6 +509,12 @@ module.exports = function(crowi, app) {
       }
     }).then(function() {
       return interceptorManager.process('beforeRenderPage', req, res, renderVars);
+    }).then(function() {
+      return Page.checkIfTemplatesExist(pageData.path)
+        .then(function(templateInfo) {
+          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
+          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
+        });
     }).then(function() {
       var defaultPageTeamplate = 'customlayout-selector/page';
       if (userData) {
@@ -549,7 +576,6 @@ module.exports = function(crowi, app) {
           return res.redirect(pagePathUtil.encodePagePath(path) + '/');
         }
         else {
-
           var fixed = Page.fixToCreatableName(path);
           if (fixed !== path) {
             debug('fixed page name', fixed);
@@ -626,7 +652,7 @@ module.exports = function(crowi, app) {
         return Page.create(path, body, req.user, { grant, grantUserGroupId });
       }
     }).then(function(data) {
-      // data is a saved page data.
+      // data is a saved page data with revision.
       pageData = data;
       if (!data) {
         throw new Error('Data not found');
@@ -638,10 +664,7 @@ module.exports = function(crowi, app) {
 
           if (crowi.slack) {
             notify.slack.channel.split(',').map(function(chan) {
-              crowi.slack.post(pageData, req.user, chan, updateOrCreate, previousRevision)
-                .catch((err) => {
-                  debug(err);
-                });
+              crowi.slack.post(pageData, req.user, chan, updateOrCreate, previousRevision);
             });
           }
         }

+ 1 - 1
lib/util/xss.js

@@ -7,7 +7,7 @@ class Xss {
     let option = {
       stripIgnoreTag: true,
       css: false,
-      escapeHtml: (html) => html,
+      escapeHtml: (html) => html,   // resolve https://github.com/weseek/growi/issues/221
     };
     if (isAllowAllAttrs) {
       // allow all attributes

+ 1 - 1
lib/views/admin/customize.html

@@ -73,7 +73,7 @@
             {# Dark Themes #}
             <div class="d-flex">
               {% include 'widget/theme-colorbox.html' with { name: 'default-dark', bg: '#212731', topbar: '#151515', theme: '#f75b36' } %}
-              {# {% include 'widget/theme-colorbox.html' with { name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE' } %} #}
+              {% include 'widget/theme-colorbox.html' with { name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE' } %}
               {% include 'widget/theme-colorbox.html' with { name: 'blue-night', bg: '#061F2F', topbar: '#27343B', theme: '#0090C8' } %}
             </div>
           </div>

+ 39 - 1
lib/views/modal/create_page.html

@@ -28,7 +28,7 @@
 
         <form class="row form-horizontal m-t-15" id="create-page-under-tree" role="form">
           <fieldset class="col-xs-12">
-            <legend>{{ t('Create under', parentPath(path)) }}</legend>
+            <legend>{{ t('Create under') }}</legend>
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
                 {% if searchConfigured() %}
@@ -44,8 +44,46 @@
           </fieldset>
         </form>
 
+        <div id = "template-form" class="row form-horizontal m-t-15">
+          <fieldset class="col-xs-12">
+            <legend>{{ t('template.modal_label.Create template under', parentPath(path)) }}</legend>
+            <div class="d-flex create-page-input-container">
+              <div class="create-page-input-row d-flex align-items-center">
+                <select id="template-type" class="page-name-input form-control">
+                  <option value="" disabled selected>{{ t('template.option_label.select') }}</option>
+                  <option value="children">{{ t('template.local.label') }}(_template) - {{ t('template.local.desc') }}</option>
+                  <option value="decentants">{{ t('template.global.label') }}(__template) - {{ t('template.global.desc') }}</option>
+                </select>
+              </div>
+              <div class="create-page-button-container">
+                  <a id="link-to-template" href="{{ page.path || path }}"><button class="fcbtn btn btn-outline btn-rounded btn-primary btn-1b"><i class="icon-fw icon-doc"></i>{{ t('Create') }}</button></a>
+              </div>
+            </div>
+          </fieldset>
+        </div>
+
       </div><!-- /.modal-body -->
 
     </div><!-- /.modal-content -->
   </div><!-- /.modal-dialog -->
 </div><!-- /.modal -->
+<script>
+  if($("#create-page")) {
+    let pagePath = $("#link-to-template").attr("href");
+
+    if (pagePath.endsWith("/")) {
+      pagePath = pagePath.slice(0, -1);
+    };
+
+    $("#template-type").on("change", () => {
+      if ($("#template-type").val() === "children") {
+        href = pagePath + "/_template#edit-form";
+        $("#link-to-template").attr("href", href);
+      }
+      else if ($("#template-type").val() === "decentants") {
+        href = pagePath + "/__template#edit-form";
+        $("#link-to-template").attr("href", href);
+      };
+    });
+  };
+</script>

+ 48 - 0
lib/views/modal/create_template.html

@@ -0,0 +1,48 @@
+<div class="modal create-template" id="create-template">
+  <div class="modal-dialog">
+    <div class="modal-content">
+
+      <div class="modal-header bg-primary">
+        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+        <div class="modal-title">{{ t('template.modal_label.Create/Edit Template Page') }}</div>
+      </div>
+      <div class="modal-body">
+        <div class="form-group">
+          <label class="mb-4">{{ t('template.modal_label.Create template under', page.path ) }}</label>
+          <div class="row">
+            <div class="col-sm-6">
+              <div class="panel panel-default panel-select-template">
+                <div class="panel-heading">{{ t('template.local.label') }}</div>
+                <div class="panel-body">
+                  <p class="text-center"><code>_template</code></p>
+                  <p class="help-block text-center"><small>{{ t('template.local.desc') }}</small></p>
+                </div>
+                <div class="panel-footer text-center">
+                  <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}_template#edit-form"
+                      class="btn btn-sm btn-primary">
+                    {% if childrenTemplateExists %}{{ t('Edit') }}{% else %}{{ t('Create') }}{% endif %}
+                  </a>
+                </div>
+              </div>
+            </div>
+            <div class="col-sm-6">
+              <div class="panel panel-default panel-select-template">
+                <div class="panel-heading">{{ t('template.global.label') }}</div>
+                <div class="panel-body">
+                  <p class="text-center"><code>__template</code></p>
+                  <p class="help-block text-center"><small>{{ t('template.global.desc') }}</small></p>
+                </div>
+                <div class="panel-footer text-center">
+                  <a href="{% if page.path.endsWith('/') %}{{ page.path }}{% else %}{{ page.path}}/{% endif %}__template#edit-form"
+                      class="btn btn-sm btn-primary">
+                  {% if decendantsTemplateExists %}{{ t('Edit') }}{% else %}{{ t('Create') }}{% endif %}
+                  </a>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div><!-- /.modal-content -->
+  </div><!-- /.modal-dialog -->
+</div><!-- /.modal -->

+ 8 - 0
lib/views/widget/not_found_content.html

@@ -17,8 +17,16 @@
 
   <div class="tab-content">
     {% if isEnabledAttachTitleHeader() %}
+    {% if template %}
+    <script type="text/template" id="raw-text-original"># {{ path|path2name }}&NewLine;{{ template }}</script>
+    {% else %}
     <script type="text/template" id="raw-text-original"># {{ path|path2name }}</script>
     {% endif %}
+    {% else %}
+    {% if template %}
+    <script type="text/template" id="raw-text-original">{{ template }}</script>
+    {% endif %}
+    {% endif %}
     {# list view #}
     <div class="p-t-10 active tab-pane page-list-container" id="revision-body">
       {% if pages.length == 0 %}

+ 1 - 0
lib/views/widget/page_modals.html

@@ -1,5 +1,6 @@
 {% include '../modal/rename.html' %}
 {% include '../modal/delete.html' %}
+{% include '../modal/create_template.html' %}
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}
 {% include '../modal/page_name_warning.html' %}

+ 14 - 10
lib/views/widget/page_tabs.html

@@ -22,36 +22,40 @@
     Right Tabs
   #}
   {% if !isTrashPage() %}
-    {% if not isPortal %}
+    {% if isPortal %}
     <li class="nav-main-right-tab dropdown pull-right">
       <a class="dropdown-toggle {% if not user %}dropdown-disabled{% endif %}" {% if user %}data-toggle="dropdown" href="#"{% endif %}>
         <i class="icon-options-vertical"></i>
       </a>
       <ul class="dropdown-menu">
-        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move') }}</a></li>
-        <li class="divider"></li>
-        <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
-        {% if isDeletablePage() %}
+        <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
+        {% if ('/' !== path) %}
         <li class="divider"></li>
-        <li class=""><a href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a></li>
+        <li><a href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a></li>
         {% endif %}
       </ul>
     </li>
-    {% elseif ('/' !== path) %}
+    {% else %}
     <li class="nav-main-right-tab dropdown pull-right">
       <a class="dropdown-toggle {% if not user %}dropdown-disabled{% endif %}" {% if user %}data-toggle="dropdown" href="#"{% endif %}>
         <i class="icon-options-vertical"></i>
       </a>
       <ul class="dropdown-menu">
-        <li><a href="#" data-target="#unportalize" data-toggle="modal"><i class="fa fa-share"></i> {{ t('Unportalize') }}</a></li>
+        <li><a href="#" data-target="#renamePage" data-toggle="modal"><i class="icon-fw icon-action-redo"></i> {{ t('Move') }}</a></li>
+        <li><a href="#" data-target="#duplicatePage" data-toggle="modal"><i class="icon-fw icon-docs"></i> {{ t('Duplicate') }}</a></li>
+        <li class="divider"></li>
+        <li><a href="#" data-target="#create-template" data-toggle="modal"><i class="icon-fw icon-magic-wand"></i> {{ t('template.option_label.create/edit') }}</a></li>
+        {% if isDeletablePage() %}
+        <li class="divider"></li>
+        <li><a href="#" data-target="#deletePage" data-toggle="modal"><i class="icon-fw icon-fire text-danger"></i> {{ t('Delete') }}</a></li>
+        {% endif %}
       </ul>
     </li>
-    {% endif %}
   {% endif %}
 
   <li class="nav-main-right-tab pull-right">
     <a href="#revision-history" data-toggle="tab">
-      <i class="icon-layers"></i><span class="hidden-xs">  History</span>
+      <i class="icon-layers"></i><span class="hidden-xs"> {{ t('History') }}</span>
     </a>
   </li>
   {% if not isPortal %}

+ 6 - 5
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.1.1-RC",
+  "version": "3.1.2-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -39,6 +39,7 @@
     "prebuild:prod": "npm run plugin:def",
     "prestart": "npm run build:prod",
     "postserver:prod:container": "echo ---------------------------------------- && echo [WARNING] && echo   'server:prod:container' is deprecated. && echo   Please use 'sever:prod' && echo ----------------------------------------",
+    "server:debug": "env-cmd config/env.dev.js node-dev --inspect app.js",
     "server:dev": "env-cmd config/env.dev.js node-dev --respawn app.js",
     "server:prod:container": "npm run server:prod",
     "server:prod:ci": "npm run server:prod -- --ci",
@@ -74,7 +75,7 @@
     "express-sanitizer": "^1.0.4",
     "express-session": "~1.15.0",
     "express-webpack-assets": "^0.1.0",
-    "googleapis": "^30.0.0",
+    "googleapis": "^31.0.2",
     "graceful-fs": "^4.1.11",
     "growi-pluginkit": "^1.1.0",
     "i18next": "^11.1.1",
@@ -104,11 +105,11 @@
     "string-width": "^2.1.1",
     "swig-templates": "^2.0.2",
     "uglifycss": "^0.0.29",
-    "xss": "^0.3.7"
+    "xss": "^1.0.3"
   },
   "devDependencies": {
     "@alienfast/i18next-loader": "^1.0.16",
-    "assets-webpack-plugin": "~3.5.1",
+    "assets-webpack-plugin": "^3.6.0",
     "autoprefixer": "^8.2.0",
     "babel-core": "^6.25.0",
     "babel-loader": "^7.1.1",
@@ -162,7 +163,7 @@
     "postcss-loader": "^2.1.3",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.0.3",
-    "react-clipboard.js": "^1.1.3",
+    "react-clipboard.js": "^2.0.0",
     "react-codemirror2": "^5.0.0",
     "react-dropzone": "^4.2.7",
     "reveal.js": "^3.5.0",

+ 9 - 13
resource/js/util/GrowiRenderer.js

@@ -1,10 +1,10 @@
 import MarkdownIt from 'markdown-it';
+import xss from 'xss';
 
 import Linker        from './PreProcessor/Linker';
 import CsvToTable    from './PreProcessor/CsvToTable';
 import XssFilter     from './PreProcessor/XssFilter';
-
-import Template from './LangProcessor/Template';
+import CrowiTemplate from './PostProcessor/CrowiTemplate';
 
 import CommonPluginsConfigurer from './markdown-it/common-plugins';
 import EmojiConfigurer from './markdown-it/emoji';
@@ -30,6 +30,8 @@ export default class GrowiRenderer {
       { isAutoSetup: true },      // default options
       options || {});             // specified options
 
+    this.xssFilterForCode = new xss.FilterXSS();
+
     // initialize processors
     //  that will be retrieved if originRenderer exists
     this.preProcessors = this.originRenderer.preProcessors || [
@@ -38,12 +40,9 @@ export default class GrowiRenderer {
       new XssFilter(crowi),
     ];
     this.postProcessors = this.originRenderer.postProcessors || [
+      new CrowiTemplate(crowi),
     ];
 
-    this.langProcessors = this.originRenderer.langProcessors || {
-      'template': new Template(crowi),
-    };
-
     this.initMarkdownItConfigurers = this.initMarkdownItConfigurers.bind(this);
     this.setup = this.setup.bind(this);
     this.process = this.process.bind(this);
@@ -147,11 +146,6 @@ export default class GrowiRenderer {
       const lang = langAndFn[0];
       const langFn = langAndFn[1] || null;
 
-      // process langProcessors
-      if (this.langProcessors[lang] != null) {
-        return this.langProcessors[lang].process(code, langExt);
-      }
-
       const citeTag = (langFn) ? `<cite>${langFn}</cite>` : '';
       if (hljs.getLanguage(lang)) {
         try {
@@ -162,11 +156,13 @@ export default class GrowiRenderer {
         }
       }
       else {
-        return `<pre class="hljs ${noborder}">${citeTag}<code>${code}</code></pre>`;
+        const escapedCode = this.xssFilterForCode.process(code);
+        return `<pre class="hljs ${noborder}">${citeTag}<code>${escapedCode}</code></pre>`;
       }
     }
 
-    return `<pre class="hljs ${noborder}"><code>${code}</code></pre>`;
+    const escapedCode = this.xssFilterForCode.process(code);
+    return `<pre class="hljs ${noborder}"><code>${escapedCode}</code></pre>`;
   }
 
 }

+ 0 - 72
resource/js/util/LangProcessor/Template.js

@@ -1,72 +0,0 @@
-import dateFnsFormat from 'date-fns/format';
-
-export default class Template {
-
-  constructor(crowi) {
-    this.templatePattern = {
-      'year': this.getYear,
-      'month': this.getMonth,
-      'date': this.getDate,
-      'user': this.getUser,
-    };
-  }
-
-  getYear() {
-    return dateFnsFormat(new Date(), 'YYYY');
-  }
-
-  getMonth() {
-    return dateFnsFormat(new Date(), 'YYYY/MM');
-  }
-
-  getDate() {
-    return dateFnsFormat(new Date(), 'YYYY/MM/DD');
-  }
-
-  getUser() {
-    // FIXME
-    const username = window.crowi.me || null;
-
-    if (!username) {
-      return '';
-    }
-
-    return `/user/${username}`;
-  }
-
-  parseTemplateString(templateString) {
-    let parsed = templateString;
-
-    Object.keys(this.templatePattern).forEach(key => {
-      const k = key .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
-      const matcher = new RegExp(`{${k}}`, 'g');
-      if (parsed.match(matcher)) {
-        const replacer = this.templatePattern[key]();
-        parsed = parsed.replace(matcher, replacer);
-      }
-    });
-
-    return parsed;
-  }
-
-  process(code, lang) {
-    const templateId = new Date().getTime().toString(16) + Math.floor(1000 * Math.random()).toString(16);
-    let pageName = lang;
-    if (lang.match(':')) {
-      pageName = this.parseTemplateString(lang.split(':')[1]);
-    }
-    code = this.parseTemplateString(code);
-
-    const content = `
-      <div class="page-template-builder">
-        <button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}">
-          <i class="fa fa-pencil"></i> ${pageName}
-        </button>
-        <pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre>
-      </div>`;
-
-    // wrap with <pre-dummy>
-    //   to avoid to be wrapped with <pre><code> by markdown-it
-    return `<pre-dummy>${content}<pre-dummy>\n`;
-  }
-}

+ 82 - 0
resource/js/util/PostProcessor/CrowiTemplate.js

@@ -0,0 +1,82 @@
+import dateFnsFormat from 'date-fns/format';
+
+export default class CrowiTemplate {
+
+  constructor(crowi) {
+    this.templatePattern = {
+      'year': this.getYear,
+      'month': this.getMonth,
+      'date': this.getDate,
+      'user': this.getUser,
+    };
+  }
+
+  process(markdown) {
+    // see: https://regex101.com/r/WR6IvX/3
+    return markdown.replace(/:::\s*(\S+)[\r\n]((.|[\r\n])*?)[\r\n]:::/gm, (all, group1, group2) => {
+      const lang = group1;
+      let code = group2;
+
+      if (!lang.match(/^template/)) {
+        return all;
+      }
+
+      const templateId = new Date().getTime().toString(16) + Math.floor(1000 * Math.random()).toString(16);
+      let pageName = lang;
+      if (lang.match(':')) {
+        pageName = this.parseTemplateString(lang.split(':')[1]);
+      }
+      code = this.parseTemplateString(code);
+
+      return (
+        /* eslint-disable quotes */
+        `<div class="page-template-builder">` +
+          `<button class="template-create-button btn btn-default" data-template="${templateId}" data-path="${pageName}">` +
+            `<i class="fa fa-pencil"></i> ${pageName}` +
+          `</button>` +
+          `<pre><code id="${templateId}" class="lang-${lang}">${code}\n</code></pre>` +
+        `</div>`
+        /* eslint-enable */
+      );
+    });
+  }
+
+  getYear() {
+    return dateFnsFormat(new Date(), 'YYYY');
+  }
+
+  getMonth() {
+    return dateFnsFormat(new Date(), 'YYYY/MM');
+  }
+
+  getDate() {
+    return dateFnsFormat(new Date(), 'YYYY/MM/DD');
+  }
+
+  getUser() {
+    // FIXME
+    const username = window.crowi.me || null;
+
+    if (!username) {
+      return '';
+    }
+
+    return `/user/${username}`;
+  }
+
+  parseTemplateString(templateString) {
+    let parsed = templateString;
+
+    Object.keys(this.templatePattern).forEach(key => {
+      const k = key .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+      const matcher = new RegExp(`{${k}}`, 'g');
+      if (parsed.match(matcher)) {
+        const replacer = this.templatePattern[key]();
+        parsed = parsed.replace(matcher, replacer);
+      }
+    });
+
+    return parsed;
+  }
+
+}

+ 0 - 9
resource/js/util/PreProcessor/ImageExpander.js

@@ -1,9 +0,0 @@
-
-export default class ImageExpander {
-
-  process(markdown) {
-
-    return markdown
-      .replace(/\s(https?:\/\/[\S]+\.(jpg|jpeg|gif|png))/g, ' <a href="$1"><img src="$1" class="auto-expanded-image"></a>');
-  }
-}

+ 2 - 2
resource/styles/agile-admin/inverse/colors/future.scss

@@ -7,8 +7,8 @@ $topbar:#011414;
 $sidebar:#fff;
 $bodycolor:$basecolor;
 $headingtext: #D9A364;
-$bodytext: #97D7CF;
-$linktext: darken($themecolor, 15%);
+$bodytext: lighten($basecolor, 35%);
+$linktext: lighten($basecolor, 45%);
 $linktext-hover: lighten($linktext, 80%);
 $sidebar-text:rgb(65, 133, 124);
 $dark-themecolor:#4F5467;

+ 1 - 1
resource/styles/scss/_create-page.scss

@@ -11,7 +11,7 @@
       margin-bottom: 10px;
     }
 
-    form {
+    form, #template-form {
 
       // layout
       .create-page-input-container {

+ 8 - 0
resource/styles/scss/_create-template.scss

@@ -0,0 +1,8 @@
+.modal.create-template {
+  @media (min-width: $screen-sm) {
+    // align .panel-body heights
+    .panel-select-template .panel-body {
+      min-height: 120px;
+    }
+  }
+}

+ 1 - 0
resource/styles/scss/style.scss

@@ -20,6 +20,7 @@
 @import 'comment';
 @import 'comment_growi';
 @import 'create-page';
+@import 'create-template';
 @import 'layout';
 @import 'layout_crowi';
 @import 'layout_crowi_sidebar';

+ 3 - 1
test/models/page.test.js

@@ -200,7 +200,9 @@ describe('Page', () => {
       expect(Page.isCreatableName('/meeting/edit')).to.be.false;
 
       // under score
-      expect(Page.isCreatableName('/_')).to.be.false;
+      expect(Page.isCreatableName('/_')).to.be.true;
+      expect(Page.isCreatableName('/_template')).to.be.true;
+      expect(Page.isCreatableName('/__template')).to.be.true;
       expect(Page.isCreatableName('/_r/x')).to.be.false;
       expect(Page.isCreatableName('/_api')).to.be.false;
       expect(Page.isCreatableName('/_apix')).to.be.false;

+ 32 - 86
yarn.lock

@@ -357,14 +357,14 @@ assertion-error@^1.0.1:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b"
 
-assets-webpack-plugin@~3.5.1:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-3.5.1.tgz#931ce0d66d42e88ed5e7f18d65522943c57a387d"
+assets-webpack-plugin@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/assets-webpack-plugin/-/assets-webpack-plugin-3.6.0.tgz#f0046def66acc728ef62ea9b8abc3716e1957adb"
   dependencies:
-    camelcase "^1.2.1"
+    camelcase "^5.0.0"
     escape-string-regexp "^1.0.3"
-    lodash.assign "^3.2.0"
-    lodash.merge "^3.3.2"
+    lodash.assign "^4.2.0"
+    lodash.merge "^4.6.1"
     mkdirp "^0.5.1"
 
 async-each-series@0.1.1:
@@ -1423,6 +1423,10 @@ camelcase@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
 
+camelcase@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.0.0.tgz#03295527d58bd3cd4aa75363f35b2e8d97be2f42"
+
 caniuse-api@^1.5.2:
   version "1.6.1"
   resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-1.6.1.tgz#b534e7c734c4f81ec5fbe8aca2ad24354b962c6c"
@@ -1597,9 +1601,9 @@ cli@~1.0.1:
     exit "0.1.2"
     glob "^7.1.1"
 
-clipboard@^1.6.1:
-  version "1.7.1"
-  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
+clipboard@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.1.tgz#a12481e1c13d8a50f5f036b0560fe5d16d74e46a"
   dependencies:
     good-listener "^1.2.2"
     select "^1.1.2"
@@ -3232,13 +3236,13 @@ google-p12-pem@^1.0.0:
     node-forge "^0.7.1"
     pify "^3.0.0"
 
-googleapis@^30.0.0:
-  version "30.0.0"
-  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-30.0.0.tgz#4673ba34878217539ca5aa4216fef4db6c247649"
+googleapis@^31.0.2:
+  version "31.0.2"
+  resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-31.0.2.tgz#03266287c8b52681e4311a28d9ff2d800e8f1afb"
   dependencies:
     google-auth-library "^1.4.0"
     pify "^3.0.0"
-    qs "^6.5.1"
+    qs "^6.5.2"
     url-template "^2.0.8"
     uuid "^3.2.1"
 
@@ -4192,30 +4196,10 @@ lodash._bindcallback@^3.0.0:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
 
-lodash._createassigner@^3.0.0:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz#838a5bae2fdaca63ac22dee8e19fa4e6d6970b11"
-  dependencies:
-    lodash._bindcallback "^3.0.0"
-    lodash._isiterateecall "^3.0.0"
-    lodash.restparam "^3.0.0"
-
 lodash._getnative@^3.0.0:
   version "3.9.1"
   resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
 
-lodash._isiterateecall@^3.0.0:
-  version "3.0.9"
-  resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c"
-
-lodash.assign@^3.2.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-3.2.0.tgz#3ce9f0234b4b2223e296b8fa0ac1fee8ebca64fa"
-  dependencies:
-    lodash._baseassign "^3.0.0"
-    lodash._createassigner "^3.0.0"
-    lodash.keys "^3.0.0"
-
 lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@@ -4259,22 +4243,10 @@ lodash.isfinite@^3.3.2:
   version "3.3.2"
   resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3"
 
-lodash.isplainobject@^3.0.0:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5"
-  dependencies:
-    lodash._basefor "^3.0.0"
-    lodash.isarguments "^3.0.0"
-    lodash.keysin "^3.0.0"
-
 lodash.isstring@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
 
-lodash.istypedarray@^3.0.0:
-  version "3.0.6"
-  resolved "https://registry.yarnpkg.com/lodash.istypedarray/-/lodash.istypedarray-3.0.6.tgz#c9a477498607501d8e8494d283b87c39281cef62"
-
 lodash.keys@^3.0.0:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -4283,41 +4255,18 @@ lodash.keys@^3.0.0:
     lodash.isarguments "^3.0.0"
     lodash.isarray "^3.0.0"
 
-lodash.keysin@^3.0.0:
-  version "3.0.8"
-  resolved "https://registry.yarnpkg.com/lodash.keysin/-/lodash.keysin-3.0.8.tgz#22c4493ebbedb1427962a54b445b2c8a767fb47f"
-  dependencies:
-    lodash.isarguments "^3.0.0"
-    lodash.isarray "^3.0.0"
-
 lodash.memoize@^4.1.2:
   version "4.1.2"
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
 
-lodash.merge@^3.3.2:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-3.3.2.tgz#0d90d93ed637b1878437bb3e21601260d7afe994"
-  dependencies:
-    lodash._arraycopy "^3.0.0"
-    lodash._arrayeach "^3.0.0"
-    lodash._createassigner "^3.0.0"
-    lodash._getnative "^3.0.0"
-    lodash.isarguments "^3.0.0"
-    lodash.isarray "^3.0.0"
-    lodash.isplainobject "^3.0.0"
-    lodash.istypedarray "^3.0.0"
-    lodash.keys "^3.0.0"
-    lodash.keysin "^3.0.0"
-    lodash.toplainobject "^3.0.0"
+lodash.merge@^4.6.1:
+  version "4.6.1"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54"
 
 lodash.mergewith@^4.6.0:
   version "4.6.0"
   resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
 
-lodash.restparam@^3.0.0:
-  version "3.6.1"
-  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
-
 lodash.set@^4.3.2:
   version "4.3.2"
   resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
@@ -4330,13 +4279,6 @@ lodash.toarray@^4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561"
 
-lodash.toplainobject@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/lodash.toplainobject/-/lodash.toplainobject-3.0.0.tgz#28790ad942d293d78aa663a07ecf7f52ca04198d"
-  dependencies:
-    lodash._basecopy "^3.0.0"
-    lodash.keysin "^3.0.0"
-
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -5881,10 +5823,14 @@ qs@6.2.1:
   version "6.2.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
 
-qs@6.5.1, qs@^6.5.1, qs@~6.5.1:
+qs@6.5.1, qs@~6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
+qs@^6.5.2:
+  version "6.5.2"
+  resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
+
 qs@~6.3.0:
   version "6.3.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
@@ -5986,11 +5932,11 @@ react-bootstrap@^0.32.1:
     uncontrollable "^4.1.0"
     warning "^3.0.0"
 
-react-clipboard.js@^1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-1.1.3.tgz#86feeb49364553ecd15aea91c75aa142532a60e0"
+react-clipboard.js@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/react-clipboard.js/-/react-clipboard.js-2.0.0.tgz#8ab3c49093e73ea146eb3bbc054889b7a60bf2b4"
   dependencies:
-    clipboard "^1.6.1"
+    clipboard "^2.0.0"
     prop-types "^15.5.0"
 
 react-codemirror2@^5.0.0:
@@ -7659,9 +7605,9 @@ xmlhttprequest-ssl@~1.5.4:
   version "1.5.4"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.4.tgz#04f560915724b389088715cc0ed7813e9677bf57"
 
-xss@^0.3.7:
-  version "0.3.8"
-  resolved "https://registry.yarnpkg.com/xss/-/xss-0.3.8.tgz#d0cbe23bde490bc98c139f08de3899165a68af0e"
+xss@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.3.tgz#d04bd2558fd6c29c46113824d5e8b2a910054e23"
   dependencies:
     commander "^2.9.0"
     cssfilter "0.0.10"