Przeglądaj źródła

Merge branch 'master' into support/2776-5975-update-diff-package

itizawa 4 lat temu
rodzic
commit
21091c3995

+ 4 - 0
CHANGES.md

@@ -2,9 +2,13 @@
 
 
 ## v4.2.19-RC
 ## v4.2.19-RC
 
 
+* Feature: Set max-age of the user's cookie with the env var `SESSION_MAX_AGE`
+* Feature: Set max-age of the user's cookie in admin page
+* Improvement: Change the first accessing page after installation to the top page
 * Support: Upgrade libs
 * Support: Upgrade libs
     * string-width
     * string-width
     * diff
     * diff
+    * archiver
 
 
 ## v4.2.18
 ## v4.2.18
 
 

+ 1 - 1
package.json

@@ -83,7 +83,7 @@
     "@slack/web-api": "^6.2.3",
     "@slack/web-api": "^6.2.3",
     "@slack/webhook": "^6.0.0",
     "@slack/webhook": "^6.0.0",
     "JSONStream": "^1.3.5",
     "JSONStream": "^1.3.5",
-    "archiver": "^3.1.1",
+    "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",
     "array.prototype.flatmap": "^1.2.2",
     "async-canvas-to-blob": "^1.0.3",
     "async-canvas-to-blob": "^1.0.3",
     "aws-sdk": "^2.88.0",
     "aws-sdk": "^2.88.0",

+ 2 - 1
resource/locales/en_US/meta.json

@@ -1,4 +1,5 @@
 {
 {
   "id": "en_US",
   "id": "en_US",
-  "displayName": "English"
+  "displayName": "English",
+  "aliases": ["en"]
 }
 }

+ 6 - 3
resource/locales/en_US/translation.json

@@ -125,13 +125,11 @@
   "Only me": "Only me",
   "Only me": "Only me",
   "Only inside the group": "Only inside the group",
   "Only inside the group": "Only inside the group",
   "page_list": "Page List",
   "page_list": "Page List",
-  "page_list_and_search_results": "Page list / Search results",
   "scope_of_page_disclosure": "Scope of page disclosure",
   "scope_of_page_disclosure": "Scope of page disclosure",
   "set_point": "Set point",
   "set_point": "Set point",
   "always_displayed": "Always displayed",
   "always_displayed": "Always displayed",
   "always_hidden": "Always hidden",
   "always_hidden": "Always hidden",
   "displayed_or_hidden": "Displayed / Hidden",
   "displayed_or_hidden": "Displayed / Hidden",
-  "page_access_and_delete_rights": "Page access / Delete rights",
   "Reselect the group": "Reselect the group",
   "Reselect the group": "Reselect the group",
   "Shareable link": "Shareable link",
   "Shareable link": "Shareable link",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
   "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
@@ -528,7 +526,6 @@
     "delete_completely": "Delete completely"
     "delete_completely": "Delete completely"
   },
   },
   "security_setting": {
   "security_setting": {
-    "Security settings": "Security settings",
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
     "Register limitation": "Register limitation",
@@ -540,15 +537,21 @@
     "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
     "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
     "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
     "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
     "insert_single": "Please insert single e-mail address per line.",
     "insert_single": "Please insert single e-mail address per line.",
+    "page_list_and_search_results": "Page list / Search results",
     "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
     "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
     "page_listing_1_desc": "Show pages that are restricted by 'Only me' option when listing/searching",
     "page_listing_1_desc": "Show pages that are restricted by 'Only me' option when listing/searching",
     "page_listing_2": "Page listing/searching<br>restricted by User group",
     "page_listing_2": "Page listing/searching<br>restricted by User group",
     "page_listing_2_desc": "Show pages that are restricted by User group when listing/searching",
     "page_listing_2_desc": "Show pages that are restricted by User group when listing/searching",
+    "page_access_and_delete_rights": "Page access / Delete rights",
     "complete_deletion": "Restrict complete deletion of pages",
     "complete_deletion": "Restrict complete deletion of pages",
     "complete_deletion_explain": "Restricts users who can completely delete pages.",
     "complete_deletion_explain": "Restricts users who can completely delete pages.",
     "admin_only": "Admin only",
     "admin_only": "Admin only",
     "admin_and_author": "Admin and author",
     "admin_and_author": "Admin and author",
     "anyone": "Anyone",
     "anyone": "Anyone",
+    "session": "Session",
+    "max_age": "Max age (msec)",
+    "max_age_desc": "Specifies the number (in milliseconds) to expire users session.<br>Default: 2592000000 (30days)",
+    "max_age_caution": "Restarting the server is required after you modify this value.",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",

+ 2 - 1
resource/locales/ja_JP/meta.json

@@ -1,4 +1,5 @@
 {
 {
   "id": "ja_JP",
   "id": "ja_JP",
-  "displayName": "日本語"
+  "displayName": "日本語",
+  "aliases": ["ja"]
 }
 }

+ 6 - 2
resource/locales/ja_JP/translation.json

@@ -125,13 +125,11 @@
   "Only me": "自分のみ",
   "Only me": "自分のみ",
   "Only inside the group": "特定グループのみ",
   "Only inside the group": "特定グループのみ",
   "page_list": "ページリスト",
   "page_list": "ページリスト",
-  "page_list_and_search_results": "ページリスト・検索結果",
   "scope_of_page_disclosure": "ページの公開範囲",
   "scope_of_page_disclosure": "ページの公開範囲",
   "set_point": "設定値",
   "set_point": "設定値",
   "always_displayed": "表示 (固定)",
   "always_displayed": "表示 (固定)",
   "always_hidden": "非表示 (固定)",
   "always_hidden": "非表示 (固定)",
   "displayed_or_hidden": "表示 / 非表示",
   "displayed_or_hidden": "表示 / 非表示",
-  "page_access_and_delete_rights": "ページの閲覧・削除権限",
   "Reselect the group": "グループの再選択",
   "Reselect the group": "グループの再選択",
   "Shareable link": "このページの共有用URL",
   "Shareable link": "このページの共有用URL",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
   "The whitelist of registration permission E-mail address": "登録許可メールアドレスの<br>ホワイトリスト",
@@ -541,15 +539,21 @@
     "for_example": "例えば、",
     "for_example": "例えば、",
     "in_this_case": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
     "in_this_case": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
     "insert_single": "1行に1メールアドレス入力してください。",
     "insert_single": "1行に1メールアドレス入力してください。",
+    "page_list_and_search_results": "ページリスト・検索結果",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2_desc": "ページのリスト表示や検索結果において、特定グループにのみ閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2_desc": "ページのリスト表示や検索結果において、特定グループにのみ閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
+    "page_access_and_delete_rights": "ページの閲覧・削除権限",
     "complete_deletion": "ページの完全削除",
     "complete_deletion": "ページの完全削除",
     "complete_deletion_explain": "ページを完全に削除できるユーザーを制限します。",
     "complete_deletion_explain": "ページを完全に削除できるユーザーを制限します。",
     "admin_only": "管理者のみ可能",
     "admin_only": "管理者のみ可能",
     "admin_and_author": "管理者とページ作者が可能",
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
     "anyone": "誰でも可能",
+    "session": "セッション",
+    "max_age": "有効期間 (ミリ秒)",
+    "max_age_desc": "ユーザーのセッション情報の有効期間をミリ秒で指定できます。<br>デフォルト値: 2592000000 (30日間)",
+    "max_age_caution": "この値を変更した後は、サーバーを再起動する必要があります。",
     "Authentication mechanism settings": "認証機構設定",
     "Authentication mechanism settings": "認証機構設定",
     "setup_is_not_yet_complete":"セットアップはまだ完了してません",
     "setup_is_not_yet_complete":"セットアップはまだ完了してません",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",

+ 3 - 2
resource/locales/zh_CN/meta.json

@@ -1,4 +1,5 @@
 {
 {
 	"id": "zh_CN",
 	"id": "zh_CN",
-	"displayName": "简体中文"
-}
+	"displayName": "简体中文",
+  "aliases": ["zh","zh-HK","zh-CN","zh-TW","zh-hk","zh-cn","zh-tw"]
+}

+ 6 - 3
resource/locales/zh_CN/translation.json

@@ -134,13 +134,11 @@
 	"Only me": "只有我",
 	"Only me": "只有我",
   "Only inside the group": "仅组内",
   "Only inside the group": "仅组内",
   "page_list": "Page List",
   "page_list": "Page List",
-	"page_list_and_search_results": "页面列表/搜索结果",
 	"scope_of_page_disclosure": "页面公开范围",
 	"scope_of_page_disclosure": "页面公开范围",
 	"set_point": "设定值",
 	"set_point": "设定值",
 	"always_displayed": "始终显示",
 	"always_displayed": "始终显示",
 	"always_hidden": "总是隐藏",
 	"always_hidden": "总是隐藏",
 	"displayed_or_hidden": "显示/隐藏",
 	"displayed_or_hidden": "显示/隐藏",
-	"page_access_and_delete_rights": "页面访问/删除权限",
 	"Reselect the group": "重新选择组",
 	"Reselect the group": "重新选择组",
 	"Shareable link": "可分享链接",
 	"Shareable link": "可分享链接",
 	"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
 	"The whitelist of registration permission E-mail address": "注册许可电子邮件地址的白名单",
@@ -516,7 +514,6 @@
     "Invalid_Number_of_Date" : "You entered invalid value"
     "Invalid_Number_of_Date" : "You entered invalid value"
   },
   },
 	"security_setting": {
 	"security_setting": {
-		"Security settings": "安全设置",
 		"Guest Users Access": "来宾用户访问",
 		"Guest Users Access": "来宾用户访问",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"Fixed by env var": "这是由env var<code>%s=%s</code>修复的。",
 		"Register limitation": "注册限制",
 		"Register limitation": "注册限制",
@@ -528,15 +525,21 @@
 		"for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
 		"for_example": " 例如,如果要将注册限制为growi.org网站域,你可以写",
 		"in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
 		"in_this_case": ";在这种情况下,只有growi.org网站域将能够注册,所有其他用户将被拒绝。",
 		"insert_single": "请每行插入一个电子邮件地址。",
 		"insert_single": "请每行插入一个电子邮件地址。",
+    "page_list_and_search_results": "页面列表/搜索结果",
 		"page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
 		"page_listing_1": "页面列表/搜索<br>受“仅限我”限制",
 		"page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
 		"page_listing_1_desc": "列出/搜索时显示受“仅限我”选项限制的页面",
 		"page_listing_2": "页面列表/搜索<br>受用户组限制",
 		"page_listing_2": "页面列表/搜索<br>受用户组限制",
 		"page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
 		"page_listing_2_desc": "显示列出/搜索时受用户组限制的页面",
+    "page_access_and_delete_rights": "页面访问/删除权限",
 		"complete_deletion": "限制完全删除页面",
 		"complete_deletion": "限制完全删除页面",
 		"complete_deletion_explain": "限制可以完全删除页面的用户。",
 		"complete_deletion_explain": "限制可以完全删除页面的用户。",
 		"admin_only": "仅管理员",
 		"admin_only": "仅管理员",
 		"admin_and_author": "管理员|作者",
 		"admin_and_author": "管理员|作者",
 		"anyone": "任何人",
 		"anyone": "任何人",
+    "session": "会议",
+    "max_age": "有效期间  (msec)",
+    "max_age_desc": "指定使用户会话过期的数量(以毫秒为单位)。<br>默认值: 2592000000 (30天)",
+    "max_age_caution": "修改该值后需要重启服务器。",
 		"Authentication mechanism settings": "身份验证机制设置",
 		"Authentication mechanism settings": "身份验证机制设置",
 		"setup_is_not_yet_complete": "安装尚未完成",
 		"setup_is_not_yet_complete": "安装尚未完成",
 		"alert_siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
 		"alert_siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",

+ 4 - 4
src/client/js/components/Admin/ImportData/ImportDataPageContents.jsx

@@ -69,7 +69,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="esaTeamName"
                   name="esaTeamName"
-                  value={adminImportContainer.state.esaTeamName}
+                  value={adminImportContainer.state.esaTeamName || ''}
                   onChange={adminImportContainer.handleInputValue}
                   onChange={adminImportContainer.handleInputValue}
                 />
                 />
               </div>
               </div>
@@ -85,7 +85,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="password"
                   type="password"
                   name="esaAccessToken"
                   name="esaAccessToken"
-                  value={adminImportContainer.state.esaAccessToken}
+                  value={adminImportContainer.state.esaAccessToken || ''}
                   onChange={adminImportContainer.handleInputValue}
                   onChange={adminImportContainer.handleInputValue}
                 />
                 />
               </div>
               </div>
@@ -174,7 +174,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="text"
                   type="text"
                   name="qiitaTeamName"
                   name="qiitaTeamName"
-                  value={adminImportContainer.state.qiitaTeamName}
+                  value={adminImportContainer.state.qiitaTeamName || ''}
                   onChange={adminImportContainer.handleInputValue}
                   onChange={adminImportContainer.handleInputValue}
                 />
                 />
               </div>
               </div>
@@ -189,7 +189,7 @@ class ImportDataPageContents extends React.Component {
                   className="form-control"
                   className="form-control"
                   type="password"
                   type="password"
                   name="qiitaAccessToken"
                   name="qiitaAccessToken"
-                  value={adminImportContainer.state.qiitaAccessToken}
+                  value={adminImportContainer.state.qiitaAccessToken || ''}
                   onChange={adminImportContainer.handleInputValue}
                   onChange={adminImportContainer.handleInputValue}
                 />
                 />
               </div>
               </div>

+ 32 - 9
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -37,15 +37,14 @@ class SecuritySetting extends React.Component {
         <h2 className="alert-anchor border-bottom">
         <h2 className="alert-anchor border-bottom">
           {t('security_settings')}
           {t('security_settings')}
         </h2>
         </h2>
+
         {adminGeneralSecurityContainer.retrieveError != null && (
         {adminGeneralSecurityContainer.retrieveError != null && (
-        <div className="alert alert-danger">
-          <p>{t('Error occurred')} : {adminGeneralSecurityContainer.retrieveError}</p>
-        </div>
-          )}
+          <div className="alert alert-danger">
+            <p>{t('Error occurred')} : {adminGeneralSecurityContainer.retrieveError}</p>
+          </div>
+        )}
 
 
-        <h4 className="mt-4">
-          { t('page_list_and_search_results') }
-        </h4>
+        <h4 className="mt-4">{ t('security_setting.page_list_and_search_results') }</h4>
         <table className="table table-bordered col-lg-9 mb-5">
         <table className="table table-bordered col-lg-9 mb-5">
           <thead>
           <thead>
             <tr>
             <tr>
@@ -98,7 +97,8 @@ class SecuritySetting extends React.Component {
             </tr>
             </tr>
           </tbody>
           </tbody>
         </table>
         </table>
-        <h4>{t('page_access_and_delete_rights')}</h4>
+
+        <h4>{t('security_setting.page_access_and_delete_rights')}</h4>
         <div className="row mb-4">
         <div className="row mb-4">
           <div className="col-md-3 text-md-right py-2">
           <div className="col-md-3 text-md-right py-2">
             <strong>{t('security_setting.Guest Users Access')}</strong>
             <strong>{t('security_setting.Guest Users Access')}</strong>
@@ -142,7 +142,6 @@ class SecuritySetting extends React.Component {
             )}
             )}
           </div>
           </div>
         </div>
         </div>
-
         <div className="row mb-4">
         <div className="row mb-4">
           <div className="col-md-3 text-md-right mb-2">
           <div className="col-md-3 text-md-right mb-2">
             <strong>{t('security_setting.complete_deletion')}</strong>
             <strong>{t('security_setting.complete_deletion')}</strong>
@@ -189,6 +188,30 @@ class SecuritySetting extends React.Component {
             </div>
             </div>
           </div>
           </div>
         </div>
         </div>
+
+        <h4>{t('security_setting.session')}</h4>
+        <div className="form-group row">
+          <label className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.max_age')}</label>
+          <div className="col-md-6">
+            <input
+              className="form-control col-md-3"
+              type="text"
+              defaultValue={adminGeneralSecurityContainer.state.sessionMaxAge || ''}
+              onChange={(e) => {
+                adminGeneralSecurityContainer.setSessionMaxAge(e.target.value);
+              }}
+              placeholder="2592000000"
+            />
+            {/* eslint-disable-next-line react/no-danger */}
+            <p className="form-text text-muted" dangerouslySetInnerHTML={{ __html: t('security_setting.max_age_desc') }} />
+            <p className="card well">
+              <span className="text-warning">
+                <i className="icon-info"></i> {t('security_setting.max_age_caution')}
+              </span>
+            </p>
+          </div>
+        </div>
+
         <div className="row my-3">
         <div className="row my-3">
           <div className="text-center text-md-left offset-md-3 col-md-5">
           <div className="text-center text-md-left offset-md-3 col-md-5">
             <button type="button" className="btn btn-primary" disabled={adminGeneralSecurityContainer.retrieveError != null} onClick={this.putSecuritySetting}>
             <button type="button" className="btn btn-primary" disabled={adminGeneralSecurityContainer.retrieveError != null} onClick={this.putSecuritySetting}>

+ 5 - 1
src/client/js/components/InstallerForm.jsx

@@ -22,7 +22,11 @@ class InstallerForm extends React.Component {
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
-    this.changeLanguage(localeMetadatas[0]);
+    const meta = localeMetadatas.find(v => v.id === i18next.language);
+    if (meta == null) {
+      return this.setState({ selectedLang: localeMetadatas[0] });
+    }
+    this.setState({ selectedLang: meta });
   }
   }
 
 
   // checkUserName(event) {
   // checkUserName(event) {

+ 4 - 4
src/client/js/components/PageCreateModal.jsx

@@ -8,7 +8,7 @@ import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
 
 
-import { userPageRoot } from '@commons/util/path-utils';
+import { userPageRoot, isCreatablePage } from '@commons/util/path-utils';
 import { pathUtils } from 'growi-commons';
 import { pathUtils } from 'growi-commons';
 
 
 import AppContainer from '../services/AppContainer';
 import AppContainer from '../services/AppContainer';
@@ -24,12 +24,12 @@ const PageCreateModal = (props) => {
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
   const pathname = decodeURI(window.location.pathname);
   const pathname = decodeURI(window.location.pathname);
   const userPageRootPath = userPageRoot(appContainer.currentUser);
   const userPageRootPath = userPageRoot(appContainer.currentUser);
-  const parentPath = pathUtils.addTrailingSlash(pathname);
+  const pageNameInputInitialValue = isCreatablePage(pathname) ? pathUtils.addTrailingSlash(pathname) : '/';
   const now = format(new Date(), 'yyyy/MM/dd');
   const now = format(new Date(), 'yyyy/MM/dd');
 
 
   const [todayInput1, setTodayInput1] = useState(t('Memo'));
   const [todayInput1, setTodayInput1] = useState(t('Memo'));
   const [todayInput2, setTodayInput2] = useState('');
   const [todayInput2, setTodayInput2] = useState('');
-  const [pageNameInput, setPageNameInput] = useState(parentPath);
+  const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
   const [template, setTemplate] = useState(null);
 
 
   function transitBySubmitEvent(e, transitHandler) {
   function transitBySubmitEvent(e, transitHandler) {
@@ -162,7 +162,7 @@ const PageCreateModal = (props) => {
               {isReachable
               {isReachable
                 ? (
                 ? (
                   <PagePathAutoComplete
                   <PagePathAutoComplete
-                    initializedPath={pathname}
+                    initializedPath={pageNameInput}
                     addTrailingSlash
                     addTrailingSlash
                     onSubmit={ppacSubmitHandler}
                     onSubmit={ppacSubmitHandler}
                     onInputChange={ppacInputChangeHandler}
                     onInputChange={ppacInputChangeHandler}

+ 10 - 0
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -18,6 +18,7 @@ export default class AdminGeneralSecurityContainer extends Container {
 
 
     this.state = {
     this.state = {
       retrieveError: null,
       retrieveError: null,
+      sessionMaxAge: null,
       wikiMode: '',
       wikiMode: '',
       // set dummy value tile for using suspense
       // set dummy value tile for using suspense
       currentRestrictGuestMode: this.dummyCurrentRestrictGuestMode,
       currentRestrictGuestMode: this.dummyCurrentRestrictGuestMode,
@@ -51,6 +52,7 @@ export default class AdminGeneralSecurityContainer extends Container {
       currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
       currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
       isShowRestrictedByGroup: !generalSetting.hideRestrictedByGroup,
+      sessionMaxAge: generalSetting.sessionMaxAge,
       wikiMode: generalSetting.wikiMode,
       wikiMode: generalSetting.wikiMode,
       isLocalEnabled: generalAuth.isLocalEnabled,
       isLocalEnabled: generalAuth.isLocalEnabled,
       isLdapEnabled: generalAuth.isLdapEnabled,
       isLdapEnabled: generalAuth.isLdapEnabled,
@@ -79,6 +81,13 @@ export default class AdminGeneralSecurityContainer extends Container {
     return this.state.wikiMode === 'public' || this.state.wikiMode === 'private';
     return this.state.wikiMode === 'public' || this.state.wikiMode === 'private';
   }
   }
 
 
+  /**
+   * setter for sessionMaxAge
+   */
+  setSessionMaxAge(sessionMaxAge) {
+    this.setState({ sessionMaxAge });
+  }
+
   /**
   /**
    * Change restrictGuestMode
    * Change restrictGuestMode
    */
    */
@@ -115,6 +124,7 @@ export default class AdminGeneralSecurityContainer extends Container {
   async updateGeneralSecuritySetting() {
   async updateGeneralSecuritySetting() {
 
 
     let requestParams = {
     let requestParams = {
+      sessionMaxAge: this.state.sessionMaxAge,
       restrictGuestMode: this.state.currentRestrictGuestMode,
       restrictGuestMode: this.state.currentRestrictGuestMode,
       pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
       pageCompleteDeletionAuthority: this.state.currentPageCompleteDeletionAuthority,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,
       hideRestrictedByGroup: !this.state.isShowRestrictedByGroup,

+ 24 - 4
src/client/js/util/i18n.js

@@ -1,13 +1,22 @@
 import i18n from 'i18next';
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
 import LanguageDetector from 'i18next-browser-languagedetector';
 import { initReactI18next } from 'react-i18next';
 import { initReactI18next } from 'react-i18next';
-
 import locales from '@root/resource/locales';
 import locales from '@root/resource/locales';
 
 
+const aliasesMapping = {};
+Object.values(locales).forEach((locale) => {
+  if (locale.meta.aliases == null) {
+    return;
+  }
+  locale.meta.aliases.forEach((alias) => {
+    aliasesMapping[alias] = locale.meta.id;
+  });
+});
+
 // extract metadata list from 'resource/locales/${locale}/meta.json'
 // extract metadata list from 'resource/locales/${locale}/meta.json'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 
 
-export const i18nFactory = (userLocaleId = 'en_US') => {
+export const i18nFactory = (userLocaleId) => {
   // setup LanguageDetector
   // setup LanguageDetector
   const langDetector = new LanguageDetector();
   const langDetector = new LanguageDetector();
   langDetector.addDetector({
   langDetector.addDetector({
@@ -15,7 +24,18 @@ export const i18nFactory = (userLocaleId = 'en_US') => {
     lookup(options) {
     lookup(options) {
       return userLocaleId;
       return userLocaleId;
     },
     },
-    cacheUserlanguage(lng, options) {
+  });
+  // Wrapper to convert lang after detected from browser
+  langDetector.addDetector({
+    name: 'navigatorWrapperToConvertByAlias',
+    lookup(options) {
+      const results = langDetector.detectors.navigator.lookup(options);
+      const lang = results[0];
+      if (lang == null) {
+        return;
+      }
+
+      return aliasesMapping[lang] || lang;
     },
     },
   });
   });
 
 
@@ -29,7 +49,7 @@ export const i18nFactory = (userLocaleId = 'en_US') => {
 
 
       fallbackLng: 'en_US',
       fallbackLng: 'en_US',
       detection: {
       detection: {
-        order: ['userSettingDetector', 'querystring', 'localStorage'],
+        order: ['userSettingDetector', 'navigatorWrapperToConvertByAlias', 'querystring'],
       },
       },
 
 
       interpolation: {
       interpolation: {

+ 33 - 0
src/lib/util/path-utils.js

@@ -37,6 +37,38 @@ const isUserPage = (path) => {
   return false;
   return false;
 };
 };
 
 
+const forbiddenPages = [
+  /\^|\$|\*|\+|#|%/,
+  /^\/-\/.*/,
+  /^\/_r\/.*/,
+  /^\/_apix?(\/.*)?/,
+  /^\/?https?:\/\/.+$/, // avoid miss in renaming
+  /\/{2,}/, // avoid miss in renaming
+  /\s+\/\s+/, // avoid miss in renaming
+  /.+\/edit$/,
+  /.+\.md$/,
+  /^(\.\.)$/, // see: https://github.com/weseek/growi/issues/3582
+  /(\/\.\.)\/?/, // see: https://github.com/weseek/growi/issues/3582
+  /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags|share)(\/.*|$)/,
+];
+
+/**
+ * Whether path can be created
+ * @param {string} path
+ * @returns {boolean}
+ */
+const isCreatablePage = (path) => {
+  let isCreatable = true;
+  forbiddenPages.forEach((page) => {
+    const pageNameReg = new RegExp(page);
+    if (path.match(pageNameReg)) {
+      isCreatable = false;
+    }
+  });
+
+  return isCreatable;
+};
+
 /**
 /**
  * return user path
  * return user path
  * @param {Object} user
  * @param {Object} user
@@ -83,6 +115,7 @@ module.exports = {
   isTopPage,
   isTopPage,
   isTrashPage,
   isTrashPage,
   isUserPage,
   isUserPage,
+  isCreatablePage,
   userPageRoot,
   userPageRoot,
   convertToNewAffiliationPath,
   convertToNewAffiliationPath,
   encodeSpaces,
   encodeSpaces,

+ 3 - 3
src/server/crowi/index.js

@@ -80,8 +80,8 @@ function Crowi(rootdir) {
 Crowi.prototype.init = async function() {
 Crowi.prototype.init = async function() {
   await this.setupDatabase();
   await this.setupDatabase();
   await this.setupModels();
   await this.setupModels();
-  await this.setupSessionConfig();
   await this.setupConfigManager();
   await this.setupConfigManager();
+  await this.setupSessionConfig();
 
 
   // setup messaging services
   // setup messaging services
   await this.setupS2sMessagingService();
   await this.setupS2sMessagingService();
@@ -217,7 +217,7 @@ Crowi.prototype.setupDatabase = function() {
 
 
 Crowi.prototype.setupSessionConfig = async function() {
 Crowi.prototype.setupSessionConfig = async function() {
   const session = require('express-session');
   const session = require('express-session');
-  const sessionAge = (1000 * 3600 * 24 * 30);
+  const sessionMaxAge = this.configManager.getConfig('crowi', 'security:sessionMaxAge') || 2592000000; // default: 30days
   const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
   const redisUrl = this.env.REDISTOGO_URL || this.env.REDIS_URI || this.env.REDIS_URL || null;
   const uid = require('uid-safe').sync;
   const uid = require('uid-safe').sync;
 
 
@@ -230,7 +230,7 @@ Crowi.prototype.setupSessionConfig = async function() {
     resave: false,
     resave: false,
     saveUninitialized: true,
     saveUninitialized: true,
     cookie: {
     cookie: {
-      maxAge: sessionAge,
+      maxAge: sessionMaxAge,
     },
     },
     genid(req) {
     genid(req) {
       // return pre-defined uid when healthcheck
       // return pre-defined uid when healthcheck

+ 0 - 27
src/server/models/page.js

@@ -522,33 +522,6 @@ module.exports = function(crowi) {
     return true;
     return true;
   };
   };
 
 
-  pageSchema.statics.isCreatableName = function(name) {
-    const forbiddenPages = [
-      /\^|\$|\*|\+|#|%/,
-      /^\/-\/.*/,
-      /^\/_r\/.*/,
-      /^\/_apix?(\/.*)?/,
-      /^\/?https?:\/\/.+$/, // avoid miss in renaming
-      /\/{2,}/, // avoid miss in renaming
-      /\s+\/\s+/, // avoid miss in renaming
-      /.+\/edit$/,
-      /.+\.md$/,
-      /^(\.\.)$/, // see: https://github.com/weseek/growi/issues/3582
-      /(\/\.\.)\/?/, // see: https://github.com/weseek/growi/issues/3582
-      /^\/(installer|register|login|logout|admin|me|files|trash|paste|comments|tags|share)(\/.*|$)/,
-    ];
-
-    let isCreatable = true;
-    forbiddenPages.forEach((page) => {
-      const pageNameReg = new RegExp(page);
-      if (name.match(pageNameReg)) {
-        isCreatable = false;
-      }
-    });
-
-    return isCreatable;
-  };
-
   pageSchema.statics.fixToCreatableName = function(path) {
   pageSchema.statics.fixToCreatableName = function(path) {
     return path
     return path
       .replace(/\/\//g, '/');
       .replace(/\/\//g, '/');

+ 2 - 1
src/server/routes/apiv3/pages.js

@@ -6,6 +6,7 @@ const pathUtils = require('growi-commons').pathUtils;
 
 
 const { body } = require('express-validator');
 const { body } = require('express-validator');
 const { query } = require('express-validator');
 const { query } = require('express-validator');
+const { isCreatablePage } = require('@commons/util/path-utils');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 const router = express.Router();
 const router = express.Router();
@@ -378,7 +379,7 @@ module.exports = (crowi) => {
       socketClientId: +req.body.socketClientId || undefined,
       socketClientId: +req.body.socketClientId || undefined,
     };
     };
 
 
-    if (!Page.isCreatableName(newPagePath)) {
+    if (!isCreatablePage(newPagePath)) {
       return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath})'`, 'invalid_path'), 409);
       return res.apiv3Err(new ErrorV3(`Could not use the path '${newPagePath})'`, 'invalid_path'), 409);
     }
     }
 
 

+ 7 - 3
src/server/routes/apiv3/security-setting.js

@@ -12,6 +12,7 @@ const removeNullPropertyFromObject = require('../../../lib/util/removeNullProper
 
 
 const validator = {
 const validator = {
   generalSetting: [
   generalSetting: [
+    body('sessionMaxAge').optional({ checkFalsy: true }).trim().isInt(),
     body('restrictGuestMode').if(value => value != null).isString().isIn([
     body('restrictGuestMode').if(value => value != null).isString().isIn([
       'Deny', 'Readonly',
       'Deny', 'Readonly',
     ]),
     ]),
@@ -361,6 +362,7 @@ module.exports = (crowi) => {
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
+        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
       },
       localSetting: {
       localSetting: {
         useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
         useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
@@ -566,7 +568,8 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/GeneralSetting'
    *                  $ref: '#/components/schemas/GeneralSetting'
    */
    */
   router.put('/general-setting', loginRequiredStrictly, adminRequired, csrf, validator.generalSetting, apiV3FormValidator, async(req, res) => {
   router.put('/general-setting', loginRequiredStrictly, adminRequired, csrf, validator.generalSetting, apiV3FormValidator, async(req, res) => {
-    const requestParams = {
+    const updateData = {
+      'security:sessionMaxAge': parseInt(req.body.sessionMaxAge),
       'security:restrictGuestMode': req.body.restrictGuestMode,
       'security:restrictGuestMode': req.body.restrictGuestMode,
       'security:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
       'security:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
@@ -575,11 +578,12 @@ module.exports = (crowi) => {
     const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
     const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
     if (wikiMode === 'private' || wikiMode === 'public') {
     if (wikiMode === 'private' || wikiMode === 'public') {
       logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
       logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
-      delete requestParams['security:restrictGuestMode'];
+      delete updateData['security:restrictGuestMode'];
     }
     }
     try {
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', updateData);
       const securitySettingParams = {
       const securitySettingParams = {
+        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
         restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
         restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
         pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
         pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),

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

@@ -92,12 +92,12 @@ module.exports = function(crowi) {
     req.logIn(adminUser, (err) => {
     req.logIn(adminUser, (err) => {
       if (err) {
       if (err) {
         req.flash('successMessage', req.t('message.complete_to_install1'));
         req.flash('successMessage', req.t('message.complete_to_install1'));
-        req.session.redirectTo = '/admin/app';
+        req.session.redirectTo = '/';
         return res.redirect('/login');
         return res.redirect('/login');
       }
       }
 
 
       req.flash('successMessage', req.t('message.complete_to_install2'));
       req.flash('successMessage', req.t('message.complete_to_install2'));
-      return res.redirect('/admin/app');
+      return res.redirect('/');
     });
     });
   };
   };
 
 

+ 2 - 121
src/server/routes/page.js

@@ -1,3 +1,4 @@
+const { isCreatablePage } = require('@commons/util/path-utils');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializePageSecurely } = require('../models/serializers/page-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
@@ -478,12 +479,10 @@ module.exports = function(crowi, app) {
   actions.notFound = async function(req, res) {
   actions.notFound = async function(req, res) {
     const path = getPathFromRequest(req);
     const path = getPathFromRequest(req);
 
 
-    const isCreatable = Page.isCreatableName(path);
-
     let view;
     let view;
     const renderVars = { path };
     const renderVars = { path };
 
 
-    if (!isCreatable) {
+    if (!isCreatablePage(path)) {
       view = 'layout-growi/not_creatable';
       view = 'layout-growi/not_creatable';
     }
     }
     else if (req.isForbidden) {
     else if (req.isForbidden) {
@@ -1231,124 +1230,6 @@ module.exports = function(crowi, app) {
     return res.json(ApiResponse.success(result));
     return res.json(ApiResponse.success(result));
   };
   };
 
 
-  /**
-   * @swagger
-   *
-   *    /pages.rename:
-   *      post:
-   *        tags: [Pages, CrowiCompatibles]
-   *        operationId: renamePage
-   *        summary: /pages.rename
-   *        description: Rename page
-   *        requestBody:
-   *          content:
-   *            application/json:
-   *              schema:
-   *                properties:
-   *                  page_id:
-   *                    $ref: '#/components/schemas/Page/properties/_id'
-   *                  path:
-   *                    $ref: '#/components/schemas/Page/properties/path'
-   *                  revision_id:
-   *                    $ref: '#/components/schemas/Revision/properties/_id'
-   *                  new_path:
-   *                    type: string
-   *                    description: new path
-   *                    example: /user/alice/new_test
-   *                  create_redirect:
-   *                    type: boolean
-   *                    description: whether redirect page
-   *                required:
-   *                  - page_id
-   *        responses:
-   *          200:
-   *            description: Succeeded to rename page.
-   *            content:
-   *              application/json:
-   *                schema:
-   *                  properties:
-   *                    ok:
-   *                      $ref: '#/components/schemas/V1Response/properties/ok'
-   *                    page:
-   *                      $ref: '#/components/schemas/Page'
-   *          403:
-   *            $ref: '#/components/responses/403'
-   *          500:
-   *            $ref: '#/components/responses/500'
-   */
-  /**
-   * @api {post} /pages.rename Rename page
-   * @apiName RenamePage
-   * @apiGroup Page
-   *
-   * @apiParam {String} page_id Page Id.
-   * @apiParam {String} path
-   * @apiParam {String} revision_id
-   * @apiParam {String} new_path New path name.
-   * @apiParam {Bool} create_redirect
-   */
-  api.rename = async function(req, res) {
-    const pageId = req.body.page_id;
-    const previousRevision = req.body.revision_id || null;
-    let newPagePath = pathUtils.normalizePath(req.body.new_path);
-    const options = {
-      createRedirectPage: (req.body.create_redirect != null),
-      updateMetadata: (req.body.remain_metadata == null),
-      socketClientId: +req.body.socketClientId || undefined,
-    };
-    const isRecursively = (req.body.recursively != null);
-
-    if (!Page.isCreatableName(newPagePath)) {
-      return res.json(ApiResponse.error(`Could not use the path '${newPagePath})'`, 'invalid_path'));
-    }
-
-    // check whether path starts slash
-    newPagePath = pathUtils.addHeadingSlash(newPagePath);
-
-    const isExist = await Page.count({ path: newPagePath }) > 0;
-    if (isExist) {
-      // if page found, cannot cannot rename to that path
-      return res.json(ApiResponse.error(`'new_path=${newPagePath}' already exists`, 'already_exists'));
-    }
-
-    let page;
-
-    try {
-      page = await Page.findByIdAndViewer(pageId, req.user);
-
-      if (page == null) {
-        return res.json(ApiResponse.error(`Page '${pageId}' is not found or forbidden`, 'notfound_or_forbidden'));
-      }
-
-      if (!page.isUpdatable(previousRevision)) {
-        return res.json(ApiResponse.error('Someone could update this page, so couldn\'t delete.', 'outdated'));
-      }
-
-      page = await crowi.pageService.renamePage(page, newPagePath, req.user, options, isRecursively);
-    }
-    catch (err) {
-      logger.error(err);
-      return res.json(ApiResponse.error('Failed to update page.', 'unknown'));
-    }
-
-    const result = {};
-    result.page = page; // TODO consider to use serializePageSecurely method -- 2018.08.06 Yuki Takei
-
-    res.json(ApiResponse.success(result));
-
-    try {
-      // global notification
-      await globalNotificationService.fire(GlobalNotificationSetting.EVENT.PAGE_MOVE, page, req.user, {
-        oldPath: req.body.path,
-      });
-    }
-    catch (err) {
-      logger.error('Move notification failed', err);
-    }
-
-    return page;
-  };
-
   /**
   /**
    * @swagger
    * @swagger
    *
    *

+ 6 - 0
src/server/service/config-loader.js

@@ -242,6 +242,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.STRING,
     type:    TYPES.STRING,
     default: undefined,
     default: undefined,
   },
   },
+  SESSION_MAX_AGE: {
+    ns:      'crowi',
+    key:     'security:sessionMaxAge',
+    type:    TYPES.NUMBER,
+    default: undefined,
+  },
   USER_UPPER_LIMIT: {
   USER_UPPER_LIMIT: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'security:userUpperLimit',
     key:     'security:userUpperLimit',

+ 3 - 1
src/server/util/createGrowiPagesFromImports.js

@@ -1,3 +1,5 @@
+const { isCreatablePage } = require('@commons/util/path-utils');
+
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
 
 
@@ -18,7 +20,7 @@ module.exports = (crowi) => {
       const path = page.path;
       const path = page.path;
       const user = page.user;
       const user = page.user;
       const body = page.body;
       const body = page.body;
-      const isCreatableName = await Page.isCreatableName(path);
+      const isCreatableName = isCreatablePage(path);
       const isPageNameTaken = await Page.findByPathAndViewer(path, user);
       const isPageNameTaken = await Page.findByPathAndViewer(path, user);
 
 
       if (isCreatableName && !isPageNameTaken) {
       if (isCreatableName && !isPageNameTaken) {

+ 0 - 50
src/test/models/page.test.js

@@ -168,56 +168,6 @@ describe('Page', () => {
     });
     });
   });
   });
 
 
-  describe('.isCreatableName', () => {
-    test('should decide creatable or not', () => {
-      expect(Page.isCreatableName('/hoge')).toBeTruthy();
-
-      // edge cases
-      expect(Page.isCreatableName('/me')).toBeFalsy();
-      expect(Page.isCreatableName('/me/')).toBeFalsy();
-      expect(Page.isCreatableName('/me/x')).toBeFalsy();
-      expect(Page.isCreatableName('/meeting')).toBeTruthy();
-      expect(Page.isCreatableName('/meeting/x')).toBeTruthy();
-
-      // end with "edit"
-      expect(Page.isCreatableName('/meeting/edit')).toBeFalsy();
-
-      // under score
-      expect(Page.isCreatableName('/_')).toBeTruthy();
-      expect(Page.isCreatableName('/_template')).toBeTruthy();
-      expect(Page.isCreatableName('/__template')).toBeTruthy();
-      expect(Page.isCreatableName('/_r/x')).toBeFalsy();
-      expect(Page.isCreatableName('/_api')).toBeFalsy();
-      expect(Page.isCreatableName('/_apix')).toBeFalsy();
-      expect(Page.isCreatableName('/_api/x')).toBeFalsy();
-
-      expect(Page.isCreatableName('/hoge/xx.md')).toBeFalsy();
-
-      // relative path
-      expect(Page.isCreatableName('/..')).toBeFalsy();
-      expect(Page.isCreatableName('/../page')).toBeFalsy();
-      expect(Page.isCreatableName('/page/..')).toBeFalsy();
-      expect(Page.isCreatableName('/page/../page')).toBeFalsy();
-
-      // start with https?
-      expect(Page.isCreatableName('/http://demo.growi.org/hoge')).toBeFalsy();
-      expect(Page.isCreatableName('/https://demo.growi.org/hoge')).toBeFalsy();
-      expect(Page.isCreatableName('http://demo.growi.org/hoge')).toBeFalsy();
-      expect(Page.isCreatableName('https://demo.growi.org/hoge')).toBeFalsy();
-
-      expect(Page.isCreatableName('/ the / path / with / space')).toBeFalsy();
-
-      const forbidden = ['installer', 'register', 'login', 'logout',
-                         'admin', 'files', 'trash', 'paste', 'comments'];
-      for (let i = 0; i < forbidden.length; i++) {
-        const pn = forbidden[i];
-        expect(Page.isCreatableName(`/${pn}`)).toBeFalsy();
-        expect(Page.isCreatableName(`/${pn}/`)).toBeFalsy();
-        expect(Page.isCreatableName(`/${pn}/abc`)).toBeFalsy();
-      }
-    });
-  });
-
   describe('.isAccessiblePageByViewer', () => {
   describe('.isAccessiblePageByViewer', () => {
     describe('with a granted page', () => {
     describe('with a granted page', () => {
       test('should return true with granted user', async() => {
       test('should return true with granted user', async() => {

+ 51 - 1
src/test/util/path-utils.test.js

@@ -1,4 +1,4 @@
-const { isTopPage, convertToNewAffiliationPath } = require('../../lib/util/path-utils');
+const { isTopPage, convertToNewAffiliationPath, isCreatablePage } = require('../../lib/util/path-utils');
 
 
 
 
 describe('TopPage Path test', () => {
 describe('TopPage Path test', () => {
@@ -55,3 +55,53 @@ describe('convertToNewAffiliationPath test', () => {
     }).toThrow();
     }).toThrow();
   });
   });
 });
 });
+
+describe('isCreatablePage test', () => {
+  test('should decide creatable or not', () => {
+    expect(isCreatablePage('/hoge')).toBeTruthy();
+
+    // edge cases
+    expect(isCreatablePage('/me')).toBeFalsy();
+    expect(isCreatablePage('/me/')).toBeFalsy();
+    expect(isCreatablePage('/me/x')).toBeFalsy();
+    expect(isCreatablePage('/meeting')).toBeTruthy();
+    expect(isCreatablePage('/meeting/x')).toBeTruthy();
+
+    // end with "edit"
+    expect(isCreatablePage('/meeting/edit')).toBeFalsy();
+
+    // under score
+    expect(isCreatablePage('/_')).toBeTruthy();
+    expect(isCreatablePage('/_template')).toBeTruthy();
+    expect(isCreatablePage('/__template')).toBeTruthy();
+    expect(isCreatablePage('/_r/x')).toBeFalsy();
+    expect(isCreatablePage('/_api')).toBeFalsy();
+    expect(isCreatablePage('/_apix')).toBeFalsy();
+    expect(isCreatablePage('/_api/x')).toBeFalsy();
+
+    expect(isCreatablePage('/hoge/xx.md')).toBeFalsy();
+
+    // relative path
+    expect(isCreatablePage('/..')).toBeFalsy();
+    expect(isCreatablePage('/../page')).toBeFalsy();
+    expect(isCreatablePage('/page/..')).toBeFalsy();
+    expect(isCreatablePage('/page/../page')).toBeFalsy();
+
+    // start with https?
+    expect(isCreatablePage('/http://demo.growi.org/hoge')).toBeFalsy();
+    expect(isCreatablePage('/https://demo.growi.org/hoge')).toBeFalsy();
+    expect(isCreatablePage('http://demo.growi.org/hoge')).toBeFalsy();
+    expect(isCreatablePage('https://demo.growi.org/hoge')).toBeFalsy();
+
+    expect(isCreatablePage('/ the / path / with / space')).toBeFalsy();
+
+    const forbidden = ['installer', 'register', 'login', 'logout',
+                       'admin', 'files', 'trash', 'paste', 'comments'];
+    for (let i = 0; i < forbidden.length; i++) {
+      const pn = forbidden[i];
+      expect(isCreatablePage(`/${pn}`)).toBeFalsy();
+      expect(isCreatablePage(`/${pn}/`)).toBeFalsy();
+      expect(isCreatablePage(`/${pn}/abc`)).toBeFalsy();
+    }
+  });
+});

+ 96 - 89
yarn.lock

@@ -2583,18 +2583,18 @@ archiver-utils@^2.1.0:
     normalize-path "^3.0.0"
     normalize-path "^3.0.0"
     readable-stream "^2.0.0"
     readable-stream "^2.0.0"
 
 
-archiver@^3.1.1:
-  version "3.1.1"
-  resolved "https://registry.yarnpkg.com/archiver/-/archiver-3.1.1.tgz#9db7819d4daf60aec10fe86b16cb9258ced66ea0"
-  integrity sha512-5Hxxcig7gw5Jod/8Gq0OneVgLYET+oNHcxgWItq4TbhOzRLKNAFUb9edAftiMKXvXfCB0vbGrJdZDNq0dWMsxg==
+archiver@^5.3.0:
+  version "5.3.0"
+  resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.0.tgz#dd3e097624481741df626267564f7dd8640a45ba"
+  integrity sha512-iUw+oDwK0fgNpvveEsdQ0Ase6IIKztBJU2U0E9MzszMfmVVUyv1QJhS2ITW9ZCqx8dktAxVAjWWkKehuZE8OPg==
   dependencies:
   dependencies:
     archiver-utils "^2.1.0"
     archiver-utils "^2.1.0"
-    async "^2.6.3"
+    async "^3.2.0"
     buffer-crc32 "^0.2.1"
     buffer-crc32 "^0.2.1"
-    glob "^7.1.4"
-    readable-stream "^3.4.0"
-    tar-stream "^2.1.0"
-    zip-stream "^2.1.2"
+    readable-stream "^3.6.0"
+    readdir-glob "^1.0.0"
+    tar-stream "^2.2.0"
+    zip-stream "^4.1.0"
 
 
 are-we-there-yet@~1.1.2:
 are-we-there-yet@~1.1.2:
   version "1.1.5"
   version "1.1.5"
@@ -2777,7 +2777,7 @@ async@1.5.2, async@^1.4.0:
   version "1.5.2"
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
 
-async@>=3.2.0:
+async@>=3.2.0, async@^3.2.0:
   version "3.2.0"
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
   integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
   integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
@@ -2786,13 +2786,6 @@ async@^0.9.0:
   version "0.9.2"
   version "0.9.2"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
   resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
 
 
-async@^2.6.3:
-  version "2.6.3"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
-  integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
-  dependencies:
-    lodash "^4.17.14"
-
 async@~0.2.6:
 async@~0.2.6:
   version "0.2.10"
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
@@ -3040,6 +3033,11 @@ base64-js@^1.3.0:
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
   integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
   integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
 
 
+base64-js@^1.3.1:
+  version "1.5.1"
+  resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
+  integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
 base64id@1.0.0:
 base64id@1.0.0:
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
   resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6"
@@ -3184,12 +3182,14 @@ bl@^2.2.1:
     readable-stream "^2.3.5"
     readable-stream "^2.3.5"
     safe-buffer "^5.1.1"
     safe-buffer "^5.1.1"
 
 
-bl@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
-  integrity sha512-EUAyP5UHU5hxF8BPT0LKW8gjYLhq1DQIcneOX/pL/m2Alo+OYDQAJlHq+yseMP50Os2nHXOSic6Ss3vSQeyf4A==
+bl@^4.0.3:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
+  integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==
   dependencies:
   dependencies:
-    readable-stream "^3.0.1"
+    buffer "^5.5.0"
+    inherits "^2.0.4"
+    readable-stream "^3.4.0"
 
 
 blob@0.0.4:
 blob@0.0.4:
   version "0.0.4"
   version "0.0.4"
@@ -3501,13 +3501,13 @@ buffer@4.9.1, buffer@^4.3.0:
     ieee754 "^1.1.4"
     ieee754 "^1.1.4"
     isarray "^1.0.0"
     isarray "^1.0.0"
 
 
-buffer@^5.1.0:
-  version "5.4.2"
-  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.2.tgz#2012872776206182480eccb2c0fba5f672a2efef"
-  integrity sha512-iy9koArjAFCzGnx3ZvNA6Z0clIbbFgbdWQ0mKD3hO0krOrZh8UgA6qMKcZvwLJxS+D6iVR76+5/pV56yMNYTag==
+buffer@^5.5.0:
+  version "5.7.1"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0"
+  integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
   dependencies:
   dependencies:
-    base64-js "^1.0.2"
-    ieee754 "^1.1.4"
+    base64-js "^1.3.1"
+    ieee754 "^1.1.13"
 
 
 buffers@~0.1.1:
 buffers@~0.1.1:
   version "0.1.1"
   version "0.1.1"
@@ -4219,15 +4219,15 @@ component-inherit@0.0.3:
   version "0.0.3"
   version "0.0.3"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
   resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143"
 
 
-compress-commons@^2.1.1:
-  version "2.1.1"
-  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-2.1.1.tgz#9410d9a534cf8435e3fbbb7c6ce48de2dc2f0610"
-  integrity sha512-eVw6n7CnEMFzc3duyFVrQEuY1BlHR3rYsSztyG32ibGMW722i3C6IizEGMFmfMU+A+fALvBIwxN3czffTcdA+Q==
+compress-commons@^4.1.0:
+  version "4.1.1"
+  resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d"
+  integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ==
   dependencies:
   dependencies:
     buffer-crc32 "^0.2.13"
     buffer-crc32 "^0.2.13"
-    crc32-stream "^3.0.1"
+    crc32-stream "^4.0.2"
     normalize-path "^3.0.0"
     normalize-path "^3.0.0"
-    readable-stream "^2.3.6"
+    readable-stream "^3.6.0"
 
 
 compressible@^2.0.12:
 compressible@^2.0.12:
   version "2.0.17"
   version "2.0.17"
@@ -4472,20 +4472,21 @@ cosmiconfig@^6.0.0:
     path-type "^4.0.0"
     path-type "^4.0.0"
     yaml "^1.7.2"
     yaml "^1.7.2"
 
 
-crc32-stream@^3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-3.0.1.tgz#cae6eeed003b0e44d739d279de5ae63b171b4e85"
-  integrity sha512-mctvpXlbzsvK+6z8kJwSJ5crm7yBwrQMTybJzMw1O4lLGJqjlDCXY2Zw7KheiA6XBEcBmfLx1D88mjRGVJtY9w==
+crc-32@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
+  integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==
   dependencies:
   dependencies:
-    crc "^3.4.4"
-    readable-stream "^3.4.0"
+    exit-on-epipe "~1.0.1"
+    printj "~1.1.0"
 
 
-crc@^3.4.4:
-  version "3.8.0"
-  resolved "https://registry.yarnpkg.com/crc/-/crc-3.8.0.tgz#ad60269c2c856f8c299e2c4cc0de4556914056c6"
-  integrity sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==
+crc32-stream@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007"
+  integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w==
   dependencies:
   dependencies:
-    buffer "^5.1.0"
+    crc-32 "^1.2.0"
+    readable-stream "^3.4.0"
 
 
 create-ecdh@^4.0.0:
 create-ecdh@^4.0.0:
   version "4.0.0"
   version "4.0.0"
@@ -5962,6 +5963,11 @@ exenv@^1.2.2:
   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
   resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
   integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
   integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=
 
 
+exit-on-epipe@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692"
+  integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==
+
 exit@0.1.2, exit@^0.1.2:
 exit@0.1.2, exit@^0.1.2:
   version "0.1.2"
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@@ -7494,6 +7500,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
   dependencies:
   dependencies:
     postcss "^7.0.14"
     postcss "^7.0.14"
 
 
+ieee754@^1.1.13:
+  version "1.2.1"
+  resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
+  integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
 ieee754@^1.1.4:
 ieee754@^1.1.4:
   version "1.1.8"
   version "1.1.8"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
   resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4"
@@ -7631,7 +7642,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     once "^1.3.0"
     wrappy "1"
     wrappy "1"
 
 
-inherits@2, inherits@~2.0.0, inherits@~2.0.3:
+inherits@2, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.3:
   version "2.0.4"
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
   integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -11939,6 +11950,11 @@ pretty-format@^25.1.0:
     ansi-styles "^4.0.0"
     ansi-styles "^4.0.0"
     react-is "^16.12.0"
     react-is "^16.12.0"
 
 
+printj@~1.1.0:
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"
+  integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==
+
 private@^0.1.6:
 private@^0.1.6:
   version "0.1.8"
   version "0.1.8"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
   resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@@ -12584,7 +12600,7 @@ read-pkg@^5.2.0:
     parse-json "^5.0.0"
     parse-json "^5.0.0"
     type-fest "^0.6.0"
     type-fest "^0.6.0"
 
 
-"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
+"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@~2.3.6:
   version "2.3.6"
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
   dependencies:
@@ -12639,7 +12655,15 @@ readable-stream@^2.0.2, readable-stream@^2.2.2, readable-stream@^2.2.6, readable
     string_decoder "~1.0.3"
     string_decoder "~1.0.3"
     util-deprecate "~1.0.1"
     util-deprecate "~1.0.1"
 
 
-readable-stream@^3.0.1, readable-stream@^3.4.0:
+readable-stream@^3.1.1:
+  version "3.2.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+readable-stream@^3.4.0:
   version "3.4.0"
   version "3.4.0"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc"
   integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
   integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==
@@ -12648,14 +12672,22 @@ readable-stream@^3.0.1, readable-stream@^3.4.0:
     string_decoder "^1.1.1"
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
     util-deprecate "^1.0.1"
 
 
-readable-stream@^3.1.1:
-  version "3.2.0"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.2.0.tgz#de17f229864c120a9f56945756e4f32c4045245d"
+readable-stream@^3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
   dependencies:
   dependencies:
     inherits "^2.0.3"
     inherits "^2.0.3"
     string_decoder "^1.1.1"
     string_decoder "^1.1.1"
     util-deprecate "^1.0.1"
     util-deprecate "^1.0.1"
 
 
+readdir-glob@^1.0.0:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.1.tgz#f0e10bb7bf7bfa7e0add8baffdc54c3f7dbee6c4"
+  integrity sha512-91/k1EzZwDx6HbERR+zucygRFfiPl2zkIYZtv3Jjr6Mn7SkKcVct8aVO+sSRiGMc6fLf72du3d92/uY63YPdEA==
+  dependencies:
+    minimatch "^3.0.4"
+
 readdirp@^2.0.0:
 readdirp@^2.0.0:
   version "2.1.0"
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
   resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78"
@@ -14165,7 +14197,7 @@ string-template@>=1.0.0:
   resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
   resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
   integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
   integrity sha1-np8iM9wA8hhxjsN5oopWc+zKi5Y=
 
 
-string-width@=4.2.2:
+string-width@=4.2.2, string-width@^4.1.0, string-width@^4.2.0:
   version "4.2.2"
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
   integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
   integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@@ -14190,15 +14222,7 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^2.0.0"
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^4.0.0"
     strip-ansi "^4.0.0"
 
 
-string-width@^3.0.0:
-  version "3.0.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.0.0.tgz#5a1690a57cc78211fffd9bf24bbe24d090604eb1"
-  dependencies:
-    emoji-regex "^7.0.1"
-    is-fullwidth-code-point "^2.0.0"
-    strip-ansi "^5.0.0"
-
-string-width@^3.1.0:
+string-width@^3.0.0, string-width@^3.1.0:
   version "3.1.0"
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -14207,23 +14231,6 @@ string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.0"
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
     strip-ansi "^5.1.0"
 
 
-string-width@^4.1.0:
-  version "4.1.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff"
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^5.2.0"
-
-string-width@^4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
-  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
-  dependencies:
-    emoji-regex "^8.0.0"
-    is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.0"
-
 string.prototype.padend@^3.0.0:
 string.prototype.padend@^3.0.0:
   version "3.0.0"
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
   resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"
@@ -14674,12 +14681,12 @@ tapable@^1.0.0-beta.5, tapable@^1.1.3:
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
   integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
 
 
-tar-stream@^2.1.0:
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.1.0.tgz#d1aaa3661f05b38b5acc9b7020efdca5179a2cc3"
-  integrity sha512-+DAn4Nb4+gz6WZigRzKEZl1QuJVOLtAwwF+WUxy1fJ6X63CaGaUAxJRD2KEn1OMfcbCjySTYpNC6WmfQoIEOdw==
+tar-stream@^2.2.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
+  integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
   dependencies:
   dependencies:
-    bl "^3.0.0"
+    bl "^4.0.3"
     end-of-stream "^1.4.1"
     end-of-stream "^1.4.1"
     fs-constants "^1.0.0"
     fs-constants "^1.0.0"
     inherits "^2.0.3"
     inherits "^2.0.3"
@@ -16280,11 +16287,11 @@ z-schema@^4.1.0:
   optionalDependencies:
   optionalDependencies:
     commander "^2.7.1"
     commander "^2.7.1"
 
 
-zip-stream@^2.1.2:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-2.1.2.tgz#841efd23214b602ff49c497cba1a85d8b5fbc39c"
-  integrity sha512-ykebHGa2+uzth/R4HZLkZh3XFJzivhVsjJt8bN3GvBzLaqqrUdRacu+c4QtnUgjkkQfsOuNE1JgLKMCPNmkKgg==
+zip-stream@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79"
+  integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A==
   dependencies:
   dependencies:
     archiver-utils "^2.1.0"
     archiver-utils "^2.1.0"
-    compress-commons "^2.1.1"
-    readable-stream "^3.4.0"
+    compress-commons "^4.1.0"
+    readable-stream "^3.6.0"