Răsfoiți Sursa

Merge pull request #799 from weseek/feat/app-siteUrl-from-env

Feat/app site url from env
Yuki Takei 7 ani în urmă
părinte
comite
0509b34595

+ 4 - 4
resource/locales/en-US/translation.json

@@ -321,7 +321,7 @@
     "Site Name": "Site name",
     "Site Name": "Site name",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "header_content": "The contents entered here will be shown in the header etc.",
-    "Site URL": "Site URL",
+    "Site URL": "This is for the site URL setting. If this setting is not done, some features don't work.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "Confidential name": "Confidential name",
     "Confidential name": "Confidential name",
     "Default Language for new users": "Default Language for new users",
     "Default Language for new users": "Default Language for new users",
@@ -349,8 +349,8 @@
     "Enable plugin loading": "Enable plugin loading",
     "Enable plugin loading": "Enable plugin loading",
     "Load plugins": "Load plugins",
     "Load plugins": "Load plugins",
     "Enable": "Enable",
     "Enable": "Enable",
-    "Disable": "Disable"
-
+    "Disable": "Disable",
+    "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>%s</code> is used."
   },
   },
   "security_setting": {
   "security_setting": {
 		"Basic authentication": "Basic authentication",
 		"Basic authentication": "Basic authentication",
@@ -379,7 +379,7 @@
     "auth_mechanism": "authentication mechanism",
     "auth_mechanism": "authentication mechanism",
     "recommended": "Recommended",
     "recommended": "Recommended",
     "username_email_password": "Username, Email and Password authentication",
     "username_email_password": "Username, Email and Password authentication",
-    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Define it from %s",
+    "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the %s",
     "ldap_auth": "LDAP authentication",
     "ldap_auth": "LDAP authentication",
     "saml_auth": "SAML authentication",
     "saml_auth": "SAML authentication",
     "google_auth2": "Google OAuth authentication",
     "google_auth2": "Google OAuth authentication",

+ 5 - 4
resource/locales/ja/translation.json

@@ -88,6 +88,7 @@
   "Table of Contents": "目次",
   "Table of Contents": "目次",
   "Management Wiki Home": "Wiki管理トップ",
   "Management Wiki Home": "Wiki管理トップ",
   "App settings": "アプリ設定",
   "App settings": "アプリ設定",
+  "Site URL settings": "サイトURL設定",
   "Markdown settings": "マークダウン設定",
   "Markdown settings": "マークダウン設定",
   "Customize": "カスタマイズ",
   "Customize": "カスタマイズ",
   "Notification settings": "通知設定",
   "Notification settings": "通知設定",
@@ -334,8 +335,8 @@
     "Site Name": "サイト名",
     "Site Name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
     "header_content": "ここに入力した内容は、ヘッダー等に表示されます。",
-    "Site URL": "サイトURL",
-    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
+    "Site URL": "サイトURLを設定します。この設定が行われていない場合は一部機能が動作しません。",
+    "siteurl_help": "<code>http://</code> または <code>https://</code> から始まるサイトのURL",
     "Confidential name": "コンフィデンシャル表示",
     "Confidential name": "コンフィデンシャル表示",
     "Default Language for new users": "新規ユーザーのデフォルト設定言語",
     "Default Language for new users": "新規ユーザーのデフォルト設定言語",
     "ex): internal use only": "例: 社外秘",
     "ex): internal use only": "例: 社外秘",
@@ -362,8 +363,8 @@
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Enable plugin loading": "プラグインの読み込みを有効にします。",
     "Load plugins": "プラグインを読み込む",
     "Load plugins": "プラグインを読み込む",
     "Enable": "有効",
     "Enable": "有効",
-    "Disable": "無効"
-
+    "Disable": "無効",
+    "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>%s</code> の値を利用します"
    },
    },
 
 
   "security_setting": {
   "security_setting": {

+ 1 - 1
src/client/styles/scss/_admin.scss

@@ -126,7 +126,7 @@
     }
     }
   }
   }
 
 
-  .authentication-settings-table {
+  .settings-table {
     table-layout: fixed;
     table-layout: fixed;
 
 
     .item-name {
     .item-name {

+ 1 - 5
src/server/crowi/express-init.js

@@ -66,12 +66,8 @@ module.exports = function(crowi, app) {
     req.config = config;
     req.config = config;
     req.csrfToken = null;
     req.csrfToken = null;
 
 
-    config.crowi['app:siteUrl:fixed'] = (config.crowi['app:siteUrl'] != null)
-      ? config.crowi['app:siteUrl']                                                                         // prioritized with v3.2.4 and above
-      : (req.headers['x-forwarded-proto'] == 'https' ? 'https' : req.protocol) + '://' + req.get('host');   // auto generate (default with v3.2.3 and below)
-
     res.locals.req      = req;
     res.locals.req      = req;
-    res.locals.baseUrl  = config.crowi['app:siteUrl:fixed'];
+    res.locals.baseUrl  = crowi.configManager.getSiteUrl();
     res.locals.config   = config;
     res.locals.config   = config;
     res.locals.env      = env;
     res.locals.env      = env;
     res.locals.now      = now;
     res.locals.now      = now;

+ 0 - 1
src/server/form/admin/app.js

@@ -5,7 +5,6 @@ var form = require('express-form')
 
 
 module.exports = form(
 module.exports = form(
   field('settingForm[app:title]').trim(),
   field('settingForm[app:title]').trim(),
-  field('settingForm[app:siteUrl]').trim().required().isUrl(),
   field('settingForm[app:confidential]'),
   field('settingForm[app:confidential]'),
   field('settingForm[app:globalLang]'),
   field('settingForm[app:globalLang]'),
   field('settingForm[app:fileUpload]').trim().toBooleanStrict()
   field('settingForm[app:fileUpload]').trim().toBooleanStrict()

+ 8 - 0
src/server/form/admin/siteUrl.js

@@ -0,0 +1,8 @@
+'use strict';
+
+var form = require('express-form')
+  , field = form.field;
+
+module.exports = form(
+  field('settingForm[app:siteUrl]').trim().isUrl()
+);

+ 1 - 0
src/server/form/index.js

@@ -12,6 +12,7 @@ module.exports = {
   },
   },
   admin: {
   admin: {
     app: require('./admin/app'),
     app: require('./admin/app'),
+    siteUrl: require('./admin/siteUrl'),
     mail: require('./admin/mail'),
     mail: require('./admin/mail'),
     aws: require('./admin/aws'),
     aws: require('./admin/aws'),
     importerEsa: require('./admin/importerEsa'),
     importerEsa: require('./admin/importerEsa'),

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

@@ -604,7 +604,7 @@ module.exports = function(crowi) {
     const local_config = {
     const local_config = {
       crowi: {
       crowi: {
         title: Config.appTitle(crowi),
         title: Config.appTitle(crowi),
-        url: config.crowi['app:siteUrl:fixed'] || '',
+        url: crowi.configManager.getSiteUrl(),
       },
       },
       upload: {
       upload: {
         image: Config.isUploadable(config),
         image: Config.isUploadable(config),

+ 1 - 1
src/server/models/user.js

@@ -707,7 +707,7 @@ module.exports = function(crowi) {
                 vars: {
                 vars: {
                   email: user.email,
                   email: user.email,
                   password: user.password,
                   password: user.password,
-                  url: config.crowi['app:siteUrl:fixed'],
+                  url: crowi.configManager.getSiteUrl(),
                   appTitle: Config.appTitle(config),
                   appTitle: Config.appTitle(config),
                 }
                 }
               },
               },

+ 19 - 0
src/server/routes/admin.js

@@ -1004,6 +1004,25 @@ module.exports = function(crowi, app) {
     }
     }
   };
   };
 
 
+  actions.api.asyncAppSetting = async(req, res) => {
+    const form = req.form.settingForm;
+
+    if (!req.form.isValid) {
+      return res.json({status: false, message: req.form.errors.join('\n')});
+    }
+
+    debug('form content', form);
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('crowi', form);
+      return res.json({status: true});
+    }
+    catch (err) {
+      logger.error(err);
+      return res.json({status: false});
+    }
+  };
+
   actions.api.securitySetting = function(req, res) {
   actions.api.securitySetting = function(req, res) {
     const form = req.form.settingForm;
     const form = req.form.settingForm;
     const config = crowi.getConfig();
     const config = crowi.getConfig();

+ 1 - 2
src/server/routes/attachment.js

@@ -92,8 +92,7 @@ module.exports = function(crowi, app) {
       //   1. this is buggy (doesn't work on Win)
       //   1. this is buggy (doesn't work on Win)
       //   2. ensure backward compatibility of data
       //   2. ensure backward compatibility of data
 
 
-      // var config = crowi.getConfig();
-      // var baseUrl = (config.crowi['app:siteUrl:fixed'] || '');
+      // var baseUrl = crowi.configManager.getSiteUrl();
       return res.json(ApiResponse.success({
       return res.json(ApiResponse.success({
         attachments: attachments.map(at => {
         attachments: attachments.map(at => {
           const fileUrl = at.fileUrl;
           const fileUrl = at.fileUrl;

+ 1 - 7
src/server/routes/hackmd.js

@@ -39,13 +39,7 @@ module.exports = function(crowi, app) {
       agentScriptContentTpl = swig.compileFile(agentScriptPath);
       agentScriptContentTpl = swig.compileFile(agentScriptPath);
     }
     }
 
 
-    let origin = `${req.protocol}://${req.get('host')}`;
-
-    // use config.crowi['app:siteUrl:fixed'] when exist req.headers['x-forwarded-proto'].
-    // refs: lib/crowi/express-init.js
-    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
-      origin = config.crowi['app:siteUrl:fixed'];
-    }
+    const origin = crowi.configManager.getSiteUrl();
 
 
     // generate definitions to replace
     // generate definitions to replace
     const definitions = {
     const definitions = {

+ 7 - 6
src/server/routes/index.js

@@ -54,12 +54,13 @@ module.exports = function(crowi, app) {
   app.get('/login/google'            , login.loginGoogle);
   app.get('/login/google'            , login.loginGoogle);
   app.get('/logout'                  , logout.logout);
   app.get('/logout'                  , logout.logout);
 
 
-  app.get('/admin'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
-  app.get('/admin/app'                  , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
-  app.post('/_api/admin/settings/app'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
-  app.post('/_api/admin/settings/mail'  , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
-  app.post('/_api/admin/settings/aws'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
-  app.post('/_api/admin/settings/plugin', loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.plugin, admin.api.appSetting);
+  app.get('/admin'                          , loginRequired(crowi, app) , middleware.adminRequired() , admin.index);
+  app.get('/admin/app'                      , loginRequired(crowi, app) , middleware.adminRequired() , admin.app.index);
+  app.post('/_api/admin/settings/app'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.app, admin.api.appSetting);
+  app.post('/_api/admin/settings/siteUrl'   , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.siteUrl, admin.api.asyncAppSetting);
+  app.post('/_api/admin/settings/mail'      , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.mail, admin.api.appSetting);
+  app.post('/_api/admin/settings/aws'       , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.aws, admin.api.appSetting);
+  app.post('/_api/admin/settings/plugin'    , loginRequired(crowi, app) , middleware.adminRequired() , csrf, form.admin.plugin, admin.api.appSetting);
 
 
   // security admin
   // security admin
   app.get('/admin/security'                     , loginRequired(crowi, app) , middleware.adminRequired() , admin.security.index);
   app.get('/admin/security'                     , loginRequired(crowi, app) , middleware.adminRequired() , admin.security.index);

+ 4 - 4
src/server/routes/login.js

@@ -106,7 +106,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.loginGoogle = function(req, res) {
   actions.loginGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     var code = req.session.googleAuthCode || null;
     var code = req.session.googleAuthCode || null;
 
 
     if (!code) {
     if (!code) {
@@ -140,7 +140,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.register = function(req, res) {
   actions.register = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
 
 
     // ログイン済みならさようなら
     // ログイン済みならさようなら
     if (req.user) {
     if (req.user) {
@@ -212,7 +212,7 @@ module.exports = function(crowi, app) {
                       vars: {
                       vars: {
                         createdUser: userData,
                         createdUser: userData,
                         adminUser: adminUser,
                         adminUser: adminUser,
-                        url: config.crowi['app:siteUrl:fixed'],
+                        url: crowi.configManager.getSiteUrl(),
                         appTitle: appTitle,
                         appTitle: appTitle,
                       }
                       }
                     },
                     },
@@ -321,7 +321,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.registerGoogle = function(req, res) {
   actions.registerGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     googleAuth.createAuthUrl(req, function(err, redirectUrl) {
     googleAuth.createAuthUrl(req, function(err, redirectUrl) {
       if (err) {
       if (err) {
         // TODO
         // TODO

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

@@ -384,7 +384,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.authGoogle = function(req, res) {
   actions.authGoogle = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
 
 
     var userData = req.user;
     var userData = req.user;
 
 
@@ -413,7 +413,7 @@ module.exports = function(crowi, app) {
   };
   };
 
 
   actions.authGoogleCallback = function(req, res) {
   actions.authGoogleCallback = function(req, res) {
-    var googleAuth = require('../util/googleAuth')(config);
+    var googleAuth = require('../util/googleAuth')(crowi);
     var userData = req.user;
     var userData = req.user;
 
 
     googleAuth.handleCallback(req, function(err, tokenInfo) {
     googleAuth.handleCallback(req, function(err, tokenInfo) {

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

@@ -110,6 +110,12 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
   //   type:    ,
   //   type:    ,
   //   default:
   //   default:
   // },
   // },
+  APP_SITE_URL: {
+    ns:      'crowi',
+    key:     'app:siteUrl',
+    type:    TYPES.STRING,
+    default: null
+  },
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
   SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: {
     ns:      'crowi',
     ns:      'crowi',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',
     key:     'security:passport-saml:useOnlyEnvVarsForSomeOptions',

+ 19 - 0
src/server/service/config-manager.js

@@ -68,6 +68,25 @@ class ConfigManager {
     return this.searchOnlyFromEnvVarConfigs(namespace, key);
     return this.searchOnlyFromEnvVarConfigs(namespace, key);
   }
   }
 
 
+  /**
+   * get the site url
+   *
+   * If the config for the site url is not set, this returns a message "[The site URL is not set. Please set it!]".
+   *
+   * With version 3.2.3 and below, there is no config for the site URL, so the system always uses auto-generated site URL.
+   * With version 3.2.4 to 3.3.4, the system uses the auto-generated site URL only if the config is not set.
+   * With version 3.3.5 and above, the system use only a value from the config.
+   */
+  getSiteUrl() {
+    const siteUrl = this.getConfig('crowi', 'app:siteUrl');
+    if (siteUrl != null) {
+      return siteUrl;
+    }
+    else {
+      return '[The site URL is not set. Please set it!]';
+    }
+  }
+
   /**
   /**
    * update configs in the same namespace
    * update configs in the same namespace
    *
    *

+ 9 - 10
src/server/service/passport.js

@@ -311,8 +311,8 @@ class PassportService {
     passport.use(new GoogleStrategy({
     passport.use(new GoogleStrategy({
       clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
       clientId: config.crowi['security:passport-google:clientId'] || process.env.OAUTH_GOOGLE_CLIENT_ID,
       clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
       clientSecret: config.crowi['security:passport-google:clientSecret'] || process.env.OAUTH_GOOGLE_CLIENT_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/google/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/google/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
         : config.crowi['security:passport-google:callbackUrl'] || process.env.OAUTH_GOOGLE_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
@@ -358,8 +358,8 @@ class PassportService {
     passport.use(new GitHubStrategy({
     passport.use(new GitHubStrategy({
       clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
       clientID: config.crowi['security:passport-github:clientId'] || process.env.OAUTH_GITHUB_CLIENT_ID,
       clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
       clientSecret: config.crowi['security:passport-github:clientSecret'] || process.env.OAUTH_GITHUB_CLIENT_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/github/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/github/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
         : config.crowi['security:passport-github:callbackUrl'] || process.env.OAUTH_GITHUB_CALLBACK_URI,    // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
@@ -405,8 +405,8 @@ class PassportService {
     passport.use(new TwitterStrategy({
     passport.use(new TwitterStrategy({
       consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
       consumerKey: config.crowi['security:passport-twitter:consumerKey'] || process.env.OAUTH_TWITTER_CONSUMER_KEY,
       consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
       consumerSecret: config.crowi['security:passport-twitter:consumerSecret'] || process.env.OAUTH_TWITTER_CONSUMER_SECRET,
-      callbackURL: (config.crowi['app:siteUrl'] != null)
-        ? `${config.crowi['app:siteUrl']}/passport/twitter/callback`                                         // auto-generated with v3.2.4 and above
+      callbackURL: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/twitter/callback`                               // auto-generated with v3.2.4 and above
         : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,   // DEPRECATED: backward compatible with v3.2.3 and below
         : config.crowi['security:passport-twitter:callbackUrl'] || process.env.OAUTH_TWITTER_CALLBACK_URI,   // DEPRECATED: backward compatible with v3.2.3 and below
       skipUserProfile: false,
       skipUserProfile: false,
     }, function(accessToken, refreshToken, profile, done) {
     }, function(accessToken, refreshToken, profile, done) {
@@ -451,10 +451,9 @@ class PassportService {
     debug('SamlStrategy: setting up..');
     debug('SamlStrategy: setting up..');
     passport.use(new SamlStrategy({
     passport.use(new SamlStrategy({
       entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
       entryPoint: configManager.getConfig('crowi', 'security:passport-saml:entryPoint'),
-      callbackUrl:
-        (config.crowi['app:siteUrl'] != null)
-          ? `${config.crowi['app:siteUrl']}/passport/saml/callback`                 // auto-generated with v3.2.4 and above
-          : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'),    // DEPRECATED: backward compatible with v3.2.3 and below
+      callbackUrl: (this.crowi.configManager.getConfig('crowi', 'app:siteUrl') != null)
+        ? `${this.crowi.configManager.getSiteUrl()}/passport/saml/callback`          // auto-generated with v3.2.4 and above
+        : configManager.getConfig('crowi', 'security:passport-saml:callbackUrl'),    // DEPRECATED: backward compatible with v3.2.3 and below
       issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
       issuer: configManager.getConfig('crowi', 'security:passport-saml:issuer'),
       cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),
       cert: configManager.getConfig('crowi', 'security:passport-saml:cert'),
     }, function(profile, done) {
     }, function(profile, done) {

+ 4 - 3
src/server/util/googleAuth.js

@@ -2,12 +2,13 @@
  * googleAuth utility
  * googleAuth utility
  */
  */
 
 
-module.exports = function(config) {
+module.exports = function(crowi) {
   'use strict';
   'use strict';
 
 
   const { GoogleApis } = require('googleapis');
   const { GoogleApis } = require('googleapis');
   var google = new GoogleApis()
   var google = new GoogleApis()
     , debug = require('debug')('growi:lib:googleAuth')
     , debug = require('debug')('growi:lib:googleAuth')
+    , config = crowi.getConfig()
     , lib = {}
     , lib = {}
     ;
     ;
 
 
@@ -20,7 +21,7 @@ module.exports = function(config) {
   }
   }
 
 
   lib.createAuthUrl = function(req, callback) {
   lib.createAuthUrl = function(req, callback) {
-    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
+    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
     google.options({auth: oauth2Client});
 
 
@@ -33,7 +34,7 @@ module.exports = function(config) {
   };
   };
 
 
   lib.handleCallback = function(req, callback) {
   lib.handleCallback = function(req, callback) {
-    var callbackUrl = config.crowi['app:siteUrl:fixed'] + '/google/callback';
+    var callbackUrl = crowi.configManager.getSiteUrl() + '/google/callback';
     var oauth2Client = createOauth2Client(callbackUrl);
     var oauth2Client = createOauth2Client(callbackUrl);
     google.options({auth: oauth2Client});
     google.options({auth: oauth2Client});
 
 

+ 8 - 11
src/server/util/slack.js

@@ -44,11 +44,8 @@ module.exports = function(crowi) {
     });
     });
   };
   };
 
 
-  const convertMarkdownToMrkdwn = function(body) {
-    let url = '';
-    if (config.crowi && config.crowi['app:siteUrl:fixed']) {
-      url = config.crowi['app:siteUrl:fixed'];
-    }
+  const convertMarkdownToMarkdown = function(body) {
+    const url = crowi.configManager.getSiteUrl();
 
 
     body = body
     body = body
       .replace(/\n\*\s(.+)/g, '\n• $1')
       .replace(/\n\*\s(.+)/g, '\n• $1')
@@ -66,7 +63,7 @@ module.exports = function(crowi) {
       body = body.substr(0, 2000) + '...';
       body = body.substr(0, 2000) + '...';
     }
     }
 
 
-    return convertMarkdownToMrkdwn(body);
+    return convertMarkdownToMarkdown(body);
   };
   };
 
 
   const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
   const prepareAttachmentTextForUpdate = function(page, user, previousRevision) {
@@ -105,7 +102,7 @@ module.exports = function(crowi) {
     }
     }
 
 
     if (comment.isMarkdown) {
     if (comment.isMarkdown) {
-      return convertMarkdownToMrkdwn(body);
+      return convertMarkdownToMarkdown(body);
     }
     }
     else {
     else {
       return body;
       return body;
@@ -113,7 +110,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
   const prepareSlackMessageForPage = function(page, user, channel, updateType, previousRevision) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     let body = page.revision.body;
     let body = page.revision.body;
 
 
     if (updateType == 'create') {
     if (updateType == 'create') {
@@ -148,7 +145,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
   const prepareSlackMessageForComment = function(comment, user, channel, path) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     const body = prepareAttachmentTextForComment(comment);
     const body = prepareAttachmentTextForComment(comment);
 
 
     const attachment = {
     const attachment = {
@@ -175,7 +172,7 @@ module.exports = function(crowi) {
 
 
   const getSlackMessageTextForPage = function(path, user, updateType) {
   const getSlackMessageTextForPage = function(path, user, updateType) {
     let text;
     let text;
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
 
 
     const pageUrl = `<${url}${path}|${path}>`;
     const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
     if (updateType == 'create') {
@@ -189,7 +186,7 @@ module.exports = function(crowi) {
   };
   };
 
 
   const getSlackMessageTextForComment = function(path, user) {
   const getSlackMessageTextForComment = function(path, user) {
-    const url = config.crowi['app:siteUrl:fixed'] || '';
+    const url = crowi.configManager.getSiteUrl();
     const pageUrl = `<${url}${path}|${path}>`;
     const pageUrl = `<${url}${path}|${path}>`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
     const text = `:speech_balloon: ${user.username} commented on ${pageUrl}`;
 
 

+ 75 - 14
src/server/views/admin/app.html

@@ -48,19 +48,6 @@
           </div>
           </div>
         </div>
         </div>
 
 
-        <div class="form-group">
-          <label for="settingForm[app:siteUrl]" class="col-xs-3 control-label">{{ t('app_setting.Site URL') }}</label>
-          <div class="col-xs-6">
-            <input class="form-control"
-                   id="settingForm[app:siteUrl]"
-                   type="text"
-                   name="settingForm[app:siteUrl]"
-                   value="{{ settingForm['app:siteUrl'] | default('') }}"
-                   placeholder="{{ t('eg') }} https://my.growi.org">
-            <p class="help-block">{{ t("app_setting.siteurl_help") }}</p>
-          </div>
-        </div>
-
         <div class="form-group">
         <div class="form-group">
           <label for="settingForm[app:confidential]" class="col-xs-3 control-label">{{ t('app_setting.Confidential name') }}</label>
           <label for="settingForm[app:confidential]" class="col-xs-3 control-label">{{ t('app_setting.Confidential name') }}</label>
           <div class="col-xs-6">
           <div class="col-xs-6">
@@ -127,6 +114,54 @@
       </fieldset>
       </fieldset>
       </form>
       </form>
 
 
+      <form action="/_api/admin/settings/siteUrl" method="post" class="form-horizontal" id="siteUrlSettingForm" role="form">
+        <fieldset>
+          <legend>{{ t('Site URL settings') }}</legend>
+          <p class="well">{{ t('app_setting.Site URL') }}</p>
+
+          <div class="col-xs-offset-3">
+            <table class="table settings-table">
+              <colgroup>
+                <col class="from-db">
+                <col class="from-env-vars">
+              </colgroup>
+              <thead>
+              <tr><th>Database</th><th>Environment variables</th></tr>
+              </thead>
+              <tbody>
+                <tr>
+                  <td>
+                    <input class="form-control"
+                           type="text"
+                           name="settingForm[app:siteUrl]"
+                           value="{{ getConfigFromDB('crowi', 'app:siteUrl') | default('') }}"
+                           placeholder="e.g. https://my.growi.org">
+                    <p class="help-block">{{ t("app_setting.siteurl_help") }}</p>
+                  </td>
+                  <td>
+                    <input class="form-control"
+                           type="text"
+                           value="{{ getConfigFromEnvVars('crowi', 'app:siteUrl') | default('') }}"
+                           readonly>
+                    <p class="help-block">
+                      {{ t("app_setting.Use env var if empty", "APP_SITE_URL") }}
+                    </p>
+                  </td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+          <div class="form-group">
+            <div class="col-xs-offset-3 col-xs-6">
+              <input type="hidden" name="_csrf" value="{{ csrf() }}">
+              <button type="submit" class="btn btn-primary">{{ t('app_setting.Update') }}</button>
+            </div>
+          </div>
+        </fieldset>
+      </form>
+
+
       <form action="/_api/admin/settings/mail" method="post" class="form-horizontal" id="mailSettingForm" role="form">
       <form action="/_api/admin/settings/mail" method="post" class="form-horizontal" id="mailSettingForm" role="form">
       <fieldset>
       <fieldset>
       <legend>{{ t('app_setting.Mail settings') }}</legend>
       <legend>{{ t('app_setting.Mail settings') }}</legend>
@@ -298,7 +333,7 @@
   </div>
   </div>
 
 
   <script>
   <script>
-    $('#appSettingForm, #mailSettingForm, #awsSettingForm, #pluginSettingForm').each(function() {
+    $('#appSettingForm, #siteUrlSettingForm, #mailSettingForm, #awsSettingForm, #pluginSettingForm').each(function() {
       $(this).submit(function()
       $(this).submit(function()
       {
       {
         function showMessage(formId, msg, status) {
         function showMessage(formId, msg, status) {
@@ -346,6 +381,32 @@
       });
       });
     });
     });
 
 
+    /**
+     * The following script sets the class name 'unused' to the cell in from-env-vars column
+     * when the value of the corresponding cell from the database is not empty.
+     * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
+     *
+     * TODO The following script is duplicated from saml.html. It is desirable to integrate those in the future.
+     */
+    $('.settings-table tbody tr').each(function(_, element) {
+      const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
+      const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
+
+      // initialize
+      addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars);
+
+      // set keyup event handler
+      inputElemFromDB.keyup(function () { addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) });
+    });
+
+    function addClassToUnusedInputElemFromEnvVars(inputElemFromDB, inputElemFromEnvVars) {
+      if (inputElemFromDB.val() === '') {
+        inputElemFromEnvVars.parent().removeClass('unused');
+      }
+      else {
+        inputElemFromEnvVars.parent().addClass('unused');
+      }
+    };
   </script>
   </script>
 
 
 </div>
 </div>

+ 2 - 2
src/server/views/admin/widget/passport/github.html

@@ -4,7 +4,7 @@
 
 
   {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
   {% set nameForIsGitHubEnabled = "settingForm[security:passport-github:isEnabled]" %}
   {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
   {% set isGitHubEnabled = settingForm['security:passport-github:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/github/callback' %}
   {% set callbackUrl = siteUrl + '/passport/github/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
@@ -53,7 +53,7 @@
       <div class="col-xs-6">
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>
         </div>

+ 2 - 2
src/server/views/admin/widget/passport/google-oauth.html

@@ -4,7 +4,7 @@
 
 
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set nameForIsGoogleEnabled = "settingForm[security:passport-google:isEnabled]" %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
   {% set isGoogleEnabled = settingForm['security:passport-google:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/google/callback' %}
   {% set callbackUrl = siteUrl + '/passport/google/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
@@ -53,7 +53,7 @@
       <div class="col-xs-6">
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>
         </div>

+ 5 - 5
src/server/views/admin/widget/passport/saml.html

@@ -5,7 +5,7 @@
   {% set nameForIsSamlEnabled = "settingForm[security:passport-saml:isEnabled]" %}
   {% set nameForIsSamlEnabled = "settingForm[security:passport-saml:isEnabled]" %}
   {% set isSamlEnabled  = getConfig('crowi', 'security:passport-saml:isEnabled') %}
   {% set isSamlEnabled  = getConfig('crowi', 'security:passport-saml:isEnabled') %}
   {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions') %}
   {% set useOnlyEnvVars = getConfig('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions') %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/saml/callback' %}
   {% set callbackUrl = siteUrl + '/passport/saml/callback' %}
 
 
   {% if useOnlyEnvVars %}
   {% if useOnlyEnvVars %}
@@ -44,7 +44,7 @@
              value="{{ callbackUrl }}"
              value="{{ callbackUrl }}"
              readonly>
              readonly>
       <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'SAML Identity') }}</p>
       <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'SAML Identity') }}</p>
-      {% if !settingForm['app:siteUrl'] %}
+      {% if !getConfig('crowi', 'app:siteUrl') %}
       <div class="alert alert-danger">
       <div class="alert alert-danger">
         <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
       </div>
       </div>
@@ -67,7 +67,7 @@
     {% endif %}
     {% endif %}
 
 
     <h4>Basic Settings</h4>
     <h4>Basic Settings</h4>
-    <table class="table authentication-settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
       <colgroup>
       <colgroup>
         <col class="item-name">
         <col class="item-name">
         <col class="from-db">
         <col class="from-db">
@@ -164,7 +164,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
 
 
     <h4>Attribute Mapping</h4>
     <h4>Attribute Mapping</h4>
 
 
-    <table class="table authentication-settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+    <table class="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
       <colgroup>
       <colgroup>
         <col class="item-name">
         <col class="item-name">
         <col class="from-db">
         <col class="from-db">
@@ -384,7 +384,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
    * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
    * It is used to indicate that the system does not use a value from the environment variables by setting a css style.
    * This behavior is disabled when the system is in the use-only-env-vars mode.
    * This behavior is disabled when the system is in the use-only-env-vars mode.
    */
    */
-  $('.authentication-settings-table:not(.use-only-env-vars) tbody tr').each(function(_, element) {
+  $('.settings-table:not(.use-only-env-vars) tbody tr').each(function(_, element) {
     const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
     const inputElemFromDB      = $('td:nth-of-type(1) input[type="text"], td:nth-of-type(1) textarea', element);
     const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
     const inputElemFromEnvVars = $('td:nth-of-type(2) input[type="text"], td:nth-of-type(2) textarea', element);
 
 

+ 2 - 2
src/server/views/admin/widget/passport/twitter.html

@@ -4,7 +4,7 @@
 
 
   {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
   {% set nameForIsTwitterEnabled = "settingForm[security:passport-twitter:isEnabled]" %}
   {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
   {% set isTwitterEnabled = settingForm['security:passport-twitter:isEnabled'] %}
-  {% set siteUrl = settingForm['app:siteUrl'] || '[INVALID]' %}
+  {% set siteUrl = getConfig('crowi', 'app:siteUrl') || '[INVALID]' %}
   {% set callbackUrl = siteUrl + '/passport/twitter/callback' %}
   {% set callbackUrl = siteUrl + '/passport/twitter/callback' %}
 
 
   <div class="form-group">
   <div class="form-group">
@@ -55,7 +55,7 @@
       <div class="col-xs-6">
       <div class="col-xs-6">
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
           <input class="form-control" type="text" value="{{ callbackUrl }}" readonly>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
         <p class="help-block small">{{ t("security_setting.desc_of_callback_URL", 'OAuth') }}</p>
-        {% if !settingForm['app:siteUrl'] %}
+        {% if !getConfig('crowi', 'app:siteUrl') %}
         <div class="alert alert-danger">
         <div class="alert alert-danger">
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
           <i class="icon-exclamation"></i> {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
         </div>
         </div>

+ 7 - 1
src/server/views/layout/layout.html

@@ -117,7 +117,7 @@
 <div id="wrapper">
 <div id="wrapper">
   <!-- Navigation -->
   <!-- Navigation -->
   {% block layout_head_nav %}
   {% block layout_head_nav %}
-  <nav class="navbar navbar-default navbar-static-top m-b-0">
+  <nav class="navbar navbar-default navbar-static-top mb-0">
     <div class="navbar-header">
     <div class="navbar-header">
       <a class="navbar-toggle hidden-sm hidden-md hidden-lg " href="javascript:void(0)" data-toggle="collapse" data-target=".navbar-collapse">
       <a class="navbar-toggle hidden-sm hidden-md hidden-lg " href="javascript:void(0)" data-toggle="collapse" data-target=".navbar-collapse">
         <i class="ti-menu"></i>
         <i class="ti-menu"></i>
@@ -195,6 +195,12 @@
   {% include '../modal/create_page.html' %}
   {% include '../modal/create_page.html' %}
   {% endblock  %} {# layout_head_nav #}
   {% endblock  %} {# layout_head_nav #}
 
 
+  {% if !getConfig('crowi', 'app:siteUrl') %}
+  <div class="alert alert-warning m-b-0">
+    {{ t("security_setting.alert_siteUrl_is_not_set", '<a href="/admin/app">' + t('App settings') + '<i class="icon-login"></i></a>') }}
+  </div>
+  {% endif %}
+
   {% block sidebar %}
   {% block sidebar %}
   <!-- Left navbar-header -->
   <!-- Left navbar-header -->
   <div class="navbar-default sidebar hidden-print" role="navigation">
   <div class="navbar-default sidebar hidden-print" role="navigation">

+ 1 - 1
src/server/views/modal/duplicate.html

@@ -16,7 +16,7 @@
             <div class="form-group">
             <div class="form-group">
               <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
               <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
               <div class="input-group">
               <div class="input-group">
-                <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
+                <span class="input-group-addon">{{ baseUrl }}</span>
                 <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
                 <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
               </div>
               </div>
             </div>
             </div>

+ 1 - 1
src/server/views/modal/rename.html

@@ -16,7 +16,7 @@
           <div class="form-group">
           <div class="form-group">
             <label for="newPageName">{{ t('modal_rename.label.New page name') }}</label><br>
             <label for="newPageName">{{ t('modal_rename.label.New page name') }}</label><br>
             <div class="input-group">
             <div class="input-group">
-              <span class="input-group-addon">{{ config.crowi['app:siteUrl:fixed'] }}</span>
+              <span class="input-group-addon">{{ baseUrl }}</span>
               <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
               <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
             </div>
             </div>
           </div>
           </div>