Răsfoiți Sursa

Merge branch 'master' into imprv/auto-template-api

sou 8 ani în urmă
părinte
comite
4194517195

+ 16 - 0
CHANGES.md

@@ -3,11 +3,27 @@ CHANGES
 
 ## 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
 

+ 21 - 21
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,21 +527,21 @@ module.exports = function(crowi) {
     });
   };
 
-  // check if a given page has a local and global tempalte
+  // 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 = {
-      localTemplateExists: false,
-      globalTemplateExists: false,
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
     };
 
     return Page
       .find({path: {$in: regexpList}})
       .then(templates => {
-        templateInfo.localTemplateExists = (assignTemplateByType(templates, path, '__') ? true : false);
-        templateInfo.globalTemplateExists = (assignGlobalTemplate(templates, path) ? true : false);
+        templateInfo.childrenTemplateExists = (assignTemplateByType(templates, path, '_') ? true : false);
+        templateInfo.decendantsTemplateExists = (assignTemplateByType(templates, path, '__') ? true : false);
 
         return templateInfo;
       });
@@ -588,10 +588,10 @@ module.exports = function(crowi) {
     }
   };
 
-  const assignGlobalTemplate = (globalTemplates, path) => {
-    const globalTemplate = assignTemplateByType(globalTemplates, path, '_');
-    if (globalTemplate) {
-      return globalTemplate;
+  const assignDecendantsTemplate = (decendantsTemplates, path) => {
+    const decendantsTemplate = assignTemplateByType(decendantsTemplates, path, '__');
+    if (decendantsTemplate) {
+      return decendantsTemplate;
     }
 
     if (path === '') {
@@ -599,28 +599,28 @@ module.exports = function(crowi) {
     }
 
     const newPath = cutOffLastSlash(path);
-    return assignGlobalTemplate(globalTemplates, newPath);
+    return assignDecendantsTemplate(decendantsTemplates, newPath);
   };
 
   const fetchTemplate = (templates, templatePath) => {
     let templateBody;
     /**
-     * get local template
+     * get children template
      * __tempate: applicable only to immediate decendants
      */
-    const localTemplate = assignTemplateByType(templates, templatePath, '__');
+    const childrenTemplate = assignTemplateByType(templates, templatePath, '_');
 
     /**
-     * get global templates
+     * get decendants templates
      * _tempate: applicable to all pages under
      */
-    const globalTemplate = assignGlobalTemplate(templates, templatePath);
+    const decendantsTemplate = assignDecendantsTemplate(templates, templatePath);
 
-    if (localTemplate) {
-      templateBody =  localTemplate.revision.body;
+    if (childrenTemplate) {
+      templateBody =  childrenTemplate.revision.body;
     }
-    else if (globalTemplate) {
-      templateBody = globalTemplate.revision.body;
+    else if (decendantsTemplate) {
+      templateBody = decendantsTemplate.revision.body;
     }
 
     return templateBody;

+ 16 - 15
lib/routes/page.js

@@ -252,8 +252,8 @@ module.exports = function(crowi, app) {
       tree: [],
       pageRelatedGroup: null,
       template: null,
-      localTemplateExists: false,
-      globalTemplateExists: false,
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
     };
 
     var pageTeamplate = 'customlayout-selector/page';
@@ -286,8 +286,8 @@ module.exports = function(crowi, app) {
           return Page.checkIfTemplatesExist(originalPath);
         })
         .then(function(templateInfo) {
-          renderVars.localTemplateExists = templateInfo.localTemplateExists;
-          renderVars.globalTemplateExists = templateInfo.globalTemplateExists;
+          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
+          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
         })
         .then(() => {
           return PageGroupRelation.findByPage(renderVars.page);
@@ -443,16 +443,17 @@ module.exports = function(crowi, app) {
   function renderPage(pageData, req, res) {
     // create page
     if (!pageData) {
-      Page.findTemplate(getPathFromRequest(req))
-      .then((template) => {
-        return res.render('customlayout-selector/not_found', {
-          author: {},
-          page: false,
-          template: template,
+      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)));
     }
@@ -462,8 +463,8 @@ module.exports = function(crowi, app) {
       page: pageData,
       revision: pageData.revision || {},
       author: pageData.revision.author || false,
-      localTemplateExists: false,
-      globalTemplateExists: false,
+      childrenTemplateExists: false,
+      decendantsTemplateExists: false,
     };
     var userPage = isUserPage(pageData.path);
     var userData = null;
@@ -511,8 +512,8 @@ module.exports = function(crowi, app) {
     }).then(function() {
       return Page.checkIfTemplatesExist(pageData.path)
         .then(function(templateInfo) {
-          renderVars.localTemplateExists = templateInfo.localTemplateExists;
-          renderVars.globalTemplateExists = templateInfo.globalTemplateExists;
+          renderVars.childrenTemplateExists = templateInfo.childrenTemplateExists;
+          renderVars.decendantsTemplateExists = templateInfo.decendantsTemplateExists;
         });
     }).then(function() {
       var defaultPageTeamplate = 'customlayout-selector/page';

+ 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

+ 6 - 6
lib/views/modal/create_page.html

@@ -51,8 +51,8 @@
               <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="local">{{ t('template.local.label') }}(__template) - {{ t('template.local.desc') }}</option>
-                  <option value="global">{{ t('template.global.label') }}(_template) - {{ t('template.global.desc') }}</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">
@@ -87,12 +87,12 @@
     });
 
     $("#template-type").on("change", () => {
-      if ($("#template-type").val() === "local") {
-        href = pagePath + "/__template#edit-form";
+      if ($("#template-type").val() === "children") {
+        href = pagePath + "/_template#edit-form";
         $("#link-to-template").attr("href", href);
       }
-      else if ($("#template-type").val() === "global") {
-        href = pagePath + "/_template#edit-form";
+      else if ($("#template-type").val() === "decentants") {
+        href = pagePath + "/__template#edit-form";
         $("#link-to-template").attr("href", href);
       };
     });

+ 13 - 15
lib/views/modal/create_template.html

@@ -1,4 +1,4 @@
-<div class="modal" id="create-template">
+<div class="modal create-template" id="create-template">
   <div class="modal-dialog">
     <div class="modal-content">
 
@@ -11,34 +11,32 @@
           <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">
+              <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="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">
-                  {% if localTemplateExists %}
-                  <a href="{{ page.path }}/__template#edit-form"><button class="btn btn-sm btn-primary">{{ t('Edit') }}</button></a>
-                  {% else %}
-                  <a href="{{ page.path }}/__template#edit-form"><button class="btn btn-sm btn-primary">{{ t('Create') }}</button></a>
-                  {% endif %}
+                  <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">
+              <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="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">
-                  {% if globalTemplateExists %}
-                  <a href="{{ page.path }}/_template#edit-form"><button class="btn btn-sm btn-primary">{{ t('Edit') }}</button></a>
-                  {% else %}
-                  <a href="{{ page.path }}/_template#edit-form"><button class="btn btn-sm btn-primary">{{ t('Create') }}</button></a>
-                  {% endif %}
+                  <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>

+ 12 - 9
lib/views/widget/page_tabs.html

@@ -22,32 +22,35 @@
     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><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() %}
+        {% if ('/' !== path) %}
         <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>
+        <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">

+ 1 - 1
package.json

@@ -75,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",

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

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

+ 9 - 5
yarn.lock

@@ -3236,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"
 
@@ -5823,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"