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

Merge branch 'master' into support/2776-6048-update-archiver

itizawa 4 лет назад
Родитель
Сommit
7af4b78fe9

+ 3 - 0
CHANGES.md

@@ -2,6 +2,9 @@
 
 ## 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
     * string-width
     * archiver

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

@@ -1,4 +1,5 @@
 {
   "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 inside the group": "Only inside the group",
   "page_list": "Page List",
-  "page_list_and_search_results": "Page list / 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 / Hidden",
-  "page_access_and_delete_rights": "Page access / 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",
@@ -528,7 +526,6 @@
     "delete_completely": "Delete completely"
   },
   "security_setting": {
-    "Security settings": "Security settings",
     "Guest Users Access": "Guest users access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "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 ",
     "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.",
+    "page_list_and_search_results": "Page list / Search results",
     "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_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_access_and_delete_rights": "Page access / Delete rights",
     "complete_deletion": "Restrict complete deletion of pages",
     "complete_deletion_explain": "Restricts users who can completely delete pages.",
     "admin_only": "Admin only",
     "admin_and_author": "Admin and author",
     "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",
     "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}}",

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

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

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

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

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

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

+ 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">
           {t('security_settings')}
         </h2>
+
         {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">
           <thead>
             <tr>
@@ -98,7 +97,8 @@ class SecuritySetting extends React.Component {
             </tr>
           </tbody>
         </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="col-md-3 text-md-right py-2">
             <strong>{t('security_setting.Guest Users Access')}</strong>
@@ -142,7 +142,6 @@ class SecuritySetting extends React.Component {
             )}
           </div>
         </div>
-
         <div className="row mb-4">
           <div className="col-md-3 text-md-right mb-2">
             <strong>{t('security_setting.complete_deletion')}</strong>
@@ -189,6 +188,30 @@ class SecuritySetting extends React.Component {
             </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="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}>

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

@@ -22,7 +22,11 @@ class InstallerForm extends React.Component {
   }
 
   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) {

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

@@ -8,7 +8,7 @@ import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 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 AppContainer from '../services/AppContainer';
@@ -24,12 +24,12 @@ const PageCreateModal = (props) => {
   const isReachable = config.isSearchServiceReachable;
   const pathname = decodeURI(window.location.pathname);
   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 [todayInput1, setTodayInput1] = useState(t('Memo'));
   const [todayInput2, setTodayInput2] = useState('');
-  const [pageNameInput, setPageNameInput] = useState(parentPath);
+  const [pageNameInput, setPageNameInput] = useState(pageNameInputInitialValue);
   const [template, setTemplate] = useState(null);
 
   function transitBySubmitEvent(e, transitHandler) {
@@ -162,7 +162,7 @@ const PageCreateModal = (props) => {
               {isReachable
                 ? (
                   <PagePathAutoComplete
-                    initializedPath={pathname}
+                    initializedPath={pageNameInput}
                     addTrailingSlash
                     onSubmit={ppacSubmitHandler}
                     onInputChange={ppacInputChangeHandler}

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

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

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

@@ -1,13 +1,22 @@
 import i18n from 'i18next';
 import LanguageDetector from 'i18next-browser-languagedetector';
 import { initReactI18next } from 'react-i18next';
-
 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'
 export const localeMetadatas = Object.values(locales).map(locale => locale.meta);
 
-export const i18nFactory = (userLocaleId = 'en_US') => {
+export const i18nFactory = (userLocaleId) => {
   // setup LanguageDetector
   const langDetector = new LanguageDetector();
   langDetector.addDetector({
@@ -15,7 +24,18 @@ export const i18nFactory = (userLocaleId = 'en_US') => {
     lookup(options) {
       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',
       detection: {
-        order: ['userSettingDetector', 'querystring', 'localStorage'],
+        order: ['userSettingDetector', 'navigatorWrapperToConvertByAlias', 'querystring'],
       },
 
       interpolation: {

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

@@ -37,6 +37,38 @@ const isUserPage = (path) => {
   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
  * @param {Object} user
@@ -83,6 +115,7 @@ module.exports = {
   isTopPage,
   isTrashPage,
   isUserPage,
+  isCreatablePage,
   userPageRoot,
   convertToNewAffiliationPath,
   encodeSpaces,

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

@@ -80,8 +80,8 @@ function Crowi(rootdir) {
 Crowi.prototype.init = async function() {
   await this.setupDatabase();
   await this.setupModels();
-  await this.setupSessionConfig();
   await this.setupConfigManager();
+  await this.setupSessionConfig();
 
   // setup messaging services
   await this.setupS2sMessagingService();
@@ -217,7 +217,7 @@ Crowi.prototype.setupDatabase = function() {
 
 Crowi.prototype.setupSessionConfig = async function() {
   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 uid = require('uid-safe').sync;
 
@@ -230,7 +230,7 @@ Crowi.prototype.setupSessionConfig = async function() {
     resave: false,
     saveUninitialized: true,
     cookie: {
-      maxAge: sessionAge,
+      maxAge: sessionMaxAge,
     },
     genid(req) {
       // return pre-defined uid when healthcheck

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

@@ -522,33 +522,6 @@ module.exports = function(crowi) {
     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) {
     return path
       .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 { query } = require('express-validator');
+const { isCreatablePage } = require('@commons/util/path-utils');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 const router = express.Router();
@@ -378,7 +379,7 @@ module.exports = (crowi) => {
       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);
     }
 

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

@@ -12,6 +12,7 @@ const removeNullPropertyFromObject = require('../../../lib/util/removeNullProper
 
 const validator = {
   generalSetting: [
+    body('sessionMaxAge').optional({ checkFalsy: true }).trim().isInt(),
     body('restrictGuestMode').if(value => value != null).isString().isIn([
       'Deny', 'Readonly',
     ]),
@@ -361,6 +362,7 @@ module.exports = (crowi) => {
         hideRestrictedByOwner: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByOwner'),
         hideRestrictedByGroup: await crowi.configManager.getConfig('crowi', 'security:list-policy:hideRestrictedByGroup'),
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
+        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
       },
       localSetting: {
         useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
@@ -566,7 +568,8 @@ module.exports = (crowi) => {
    *                  $ref: '#/components/schemas/GeneralSetting'
    */
   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:pageCompleteDeletionAuthority': req.body.pageCompleteDeletionAuthority,
       'security:list-policy:hideRestrictedByOwner': req.body.hideRestrictedByOwner,
@@ -575,11 +578,12 @@ module.exports = (crowi) => {
     const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
     if (wikiMode === 'private' || wikiMode === 'public') {
       logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
-      delete requestParams['security:restrictGuestMode'];
+      delete updateData['security:restrictGuestMode'];
     }
     try {
-      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', updateData);
       const securitySettingParams = {
+        sessionMaxAge: await crowi.configManager.getConfig('crowi', 'security:sessionMaxAge'),
         restrictGuestMode: await crowi.configManager.getConfig('crowi', 'security:restrictGuestMode'),
         pageCompleteDeletionAuthority: await crowi.configManager.getConfig('crowi', 'security:pageCompleteDeletionAuthority'),
         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) => {
       if (err) {
         req.flash('successMessage', req.t('message.complete_to_install1'));
-        req.session.redirectTo = '/admin/app';
+        req.session.redirectTo = '/';
         return res.redirect('/login');
       }
 
       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 { serializeRevisionSecurely } = require('../models/serializers/revision-serializer');
 const { serializeUserSecurely } = require('../models/serializers/user-serializer');
@@ -478,12 +479,10 @@ module.exports = function(crowi, app) {
   actions.notFound = async function(req, res) {
     const path = getPathFromRequest(req);
 
-    const isCreatable = Page.isCreatableName(path);
-
     let view;
     const renderVars = { path };
 
-    if (!isCreatable) {
+    if (!isCreatablePage(path)) {
       view = 'layout-growi/not_creatable';
     }
     else if (req.isForbidden) {
@@ -1231,124 +1230,6 @@ module.exports = function(crowi, app) {
     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
    *

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

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

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

@@ -1,3 +1,5 @@
+const { isCreatablePage } = require('@commons/util/path-utils');
+
 module.exports = (crowi) => {
   const Page = crowi.model('Page');
 
@@ -18,7 +20,7 @@ module.exports = (crowi) => {
       const path = page.path;
       const user = page.user;
       const body = page.body;
-      const isCreatableName = await Page.isCreatableName(path);
+      const isCreatableName = isCreatablePage(path);
       const isPageNameTaken = await Page.findByPathAndViewer(path, user);
 
       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('with a granted page', () => {
       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', () => {
@@ -55,3 +55,53 @@ describe('convertToNewAffiliationPath test', () => {
     }).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();
+    }
+  });
+});

+ 2 - 27
yarn.lock

@@ -14196,7 +14196,7 @@ string-template@>=1.0.0:
   resolved "https://registry.yarnpkg.com/string-template/-/string-template-1.0.0.tgz#9e9f2233dc00f218718ec379a28a5673ecca8b96"
   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"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5"
   integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==
@@ -14221,15 +14221,7 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^2.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"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961"
   integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==
@@ -14238,23 +14230,6 @@ string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.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:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/string.prototype.padend/-/string.prototype.padend-3.0.0.tgz#f3aaef7c1719f170c5eab1c32bf780d96e21f2f0"