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

Merge branch 'master' into support/hotkeys

白石誠 5 лет назад
Родитель
Сommit
8a51280844

+ 25 - 1
CHANGES.md

@@ -1,9 +1,33 @@
 # CHANGES
 
-## v4.0.8-RC
+## v4.0.10-RC
 
 * 
 
+## v4.0.9
+
+* Feature: Detailed configurations for OpenID Connect
+    * Authorization Endpoint
+    * Token Endpoint
+    * Revocation Endpoint
+    * Introspection Endpoint
+    * UserInfo Endpoint
+    * Registration Endpoint
+    * JSON Web Key Set URI
+* Improvement: Navigations
+    * New floating subnavigation
+    * New open drawer button
+    * New fixed bottom navbar on mobile
+    * New fixed bottom navbar for editor on mobile
+    * FAB (Floating action button)
+* Improvement: Sticky admin navigation
+* Fix: Reseting password doesn't work
+* Fix: Styles for printing
+* Fix: Unable to create page with original path after emptying trash
+* I18n: Support zh-CN
+
+## v4.0.8  (Missing number)
+
 ## v4.0.7
 
 * Feature: Set request timeout for Elasticsearch with env var `ELASTICSEARCH_REQUEST_TIMEOUT`

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "4.0.8-RC",
+  "version": "4.0.10-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 1 - 1
resource/locales/en_US/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/en_US/translation.json

@@ -451,6 +451,14 @@
     "issuerHost": "Issuer Host",
     "scope": "Scope",
     "desc_of_callback_URL": "Use it in the setting of the {{AuthName}} Identity provider",
+    "authorization_endpoint": "Authorization Endpoint",
+    "token_endpoint": "Token Endpoint",
+    "revocation_endpoint": "Revocation Endpoint",
+    "introspection_endpoint": "Introspection Endpoint",
+    "userinfo_endpoint": "UserInfo Endpoint",
+    "end_session_endpoint": "EndSessioin Endpoint",
+    "registration_endpoint": "Registration Endpoint",
+    "jwks_uri": "JSON Web Key Set URL",
     "clientID": "Client ID",
     "client_secret": "Client Secret",
     "updated_general_security_setting": "Succeeded to update security setting",
@@ -576,7 +584,8 @@
         "register_1": "Contant to OIDC IdP Administrator",
         "register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
         "register_3": "Copy and paste your ClientID and Client Secret above",
-        "updated_oidc": "Succeeded to update OpenID Connect"
+        "updated_oidc": "Succeeded to update OpenID Connect",
+        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
       },
       "how_to": {
         "google": "How to configure Google OAuth?",

+ 1 - 1
resource/locales/ja_JP/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/ja_JP/translation.json

@@ -448,6 +448,14 @@
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
+    "authorization_endpoint": "認可エンドポイント",
+    "token_endpoint": "トークンエンドポイント",
+    "revocation_endpoint": "失効エンドポイント",
+    "introspection_endpoint": "検証エンドポイント",
+    "userinfo_endpoint": "ユーザ情報エンドポイント",
+    "end_session_endpoint": "セッション終了エンドポイント",
+    "registration_endpoint": "登録エンドポイント",
+    "jwks_uri": "JSON Web Key Set URL",
     "clientID": "クライアントID",
     "client_secret": "クライアントシークレット",
     "updated_general_security_setting": "セキュリティ設定を更新しました。",
@@ -569,7 +577,8 @@
         "username_detail": "新規ユーザーのアカウント名(<code>username</code>)に関連付ける属性",
         "name_detail": "新規ユーザー名(<code>name</code>)に関連付ける属性",
         "mapping_detail": "新規ユーザーの{{target}}に関連付ける属性",
-        "updated_oidc": "OpenID Connect を更新しました"
+        "updated_oidc": "OpenID Connect を更新しました",
+        "Use discovered URL if empty": "データベース側の値が空の場合、\"Issuer Host\"から検出した値を利用します。"
       },
       "how_to": {
         "google": "Google OAuth の設定方法",

+ 1 - 1
resource/locales/zh_CN/sandbox-math.md

@@ -4,7 +4,7 @@ See [MathJax](https://www.mathjax.org/).
 
 ## Inline Formula
 
-When $a \ne 0$, there are two solutions to \(ax^2 + bx + c = 0\) and they are
+When $a \ne 0$, there are two solutions to $ax^2 + bx + c = 0$ and they are
   $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
 
 ## The Lorenz Equations

+ 10 - 1
resource/locales/zh_CN/translation.json

@@ -440,6 +440,14 @@
 		"issuerHost": "发行者主机",
 		"scope": "Scope",
 		"desc_of_callback_URL": "在{{AuthName}}身份提供程序的设置中使用它",
+    "authorization_endpoint": "Authorization Endpoint",
+    "token_endpoint": "Token Endpoint",
+    "revocation_endpoint": "Revocation Endpoint",
+    "introspection_endpoint": "Introspection Endpoint",
+    "userinfo_endpoint": "UserInfo Endpoint",
+    "end_session_endpoint": "EndSessioin Endpoint",
+    "registration_endpoint": "Registration Endpoint",
+    "jwks_uri": "JSON Web Key Set URL",
 		"clientID": "Client ID",
 		"client_secret": "客户机密",
 		"updated_general_security_setting": "更新安全设置成功",
@@ -565,7 +573,8 @@
 				"register_1": "Contant to OIDC IdP Administrator",
 				"register_2": "Register your OIDC App with \"Authorization callback URL\" as <code>%s</code>",
 				"register_3": "Copy and paste your ClientID and Client Secret above",
-				"updated_oidc": "Succeeded to update OpenID Connect"
+				"updated_oidc": "Succeeded to update OpenID Connect",
+        "Use discovered URL if empty": "Use discovered URL from \"Issuer Host\" if empty"
 			},
 			"how_to": {
 				"google": "How to configure Google OAuth?",

+ 140 - 0
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -170,6 +170,146 @@ class OidcSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcAuthorizationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.authorization_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcAuthorizationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcAuthorizationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcAuthorizationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcTokenEndpoint" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.token_endpoint')}</label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcTokenEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcTokenEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcTokenEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcRevocationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.revocation_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcRevocationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcRevocationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcRevocationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcIntrospectionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.introspection_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcIntrospectionEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcIntrospectionEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcIntrospectionEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcUserInfoEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.userinfo_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcUserInfoEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcUserInfoEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcUserInfoEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcEndSessionEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.end_session_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcEndSessionEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcEndSessionEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcEndSessionEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcRegistrationEndpoint" className="text-left text-md-right col-md-3 col-form-label">
+                {t('security_setting.registration_endpoint')}
+              </label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcRegistrationEndpoint"
+                  defaultValue={adminOidcSecurityContainer.state.oidcRegistrationEndpoint || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcRegistrationEndpoint(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
+            <div className="row mb-5 form-group">
+              <label htmlFor="oidcJWKSUri" className="text-left text-md-right col-md-3 col-form-label">{t('security_setting.jwks_uri')}</label>
+              <div className="col-md-6">
+                <input
+                  className="form-control"
+                  type="text"
+                  name="oidcJWKSUri"
+                  defaultValue={adminOidcSecurityContainer.state.oidcJWKSUri || ''}
+                  onChange={e => adminOidcSecurityContainer.changeOidcJWKSUri(e.target.value)}
+                />
+                <p className="form-text text-muted">
+                  <small dangerouslySetInnerHTML={{ __html: t('security_setting.OAuth.OIDC.Use discovered URL if empty') }} />
+                </p>
+              </div>
+            </div>
+
             <h3 className="alert-anchor border-bottom">
               Attribute Mapping ({t('security_setting.optional')})
             </h3>

+ 1 - 1
src/client/js/components/Navbar/DrawerToggler.jsx

@@ -12,7 +12,7 @@ const DrawerToggler = (props) => {
 
   const clickHandler = useCallback(() => {
     navigationContainer.toggleDrawer();
-  }, []);
+  }, [navigationContainer]);
 
   const iconClass = props.iconClass || 'icon-menu';
 

+ 1 - 1
src/client/js/components/Navbar/GrowiSubNavigationSwitcher.jsx

@@ -46,7 +46,7 @@ const GrowiSubNavigationSwitcher = (props) => {
     return () => {
       window.removeEventListener('resize', resizeHandler);
     };
-  }, []);
+  }, [resetWidth]);
 
   const stickyChangeHandler = useCallback((event) => {
     logger.debug('StickyEvents.CHANGE detected');

+ 61 - 61
src/client/js/components/PageStatusAlert.jsx

@@ -25,81 +25,64 @@ class PageStatusAlert extends React.Component {
     this.state = {
     };
 
-    this.renderSomeoneEditingAlert = this.renderSomeoneEditingAlert.bind(this);
-    this.renderDraftExistsAlert = this.renderDraftExistsAlert.bind(this);
-    this.renderUpdatedAlert = this.renderUpdatedAlert.bind(this);
+    this.getContentsForSomeoneEditingAlert = this.getContentsForSomeoneEditingAlert.bind(this);
+    this.getContentsForDraftExistsAlert = this.getContentsForDraftExistsAlert.bind(this);
+    this.getContentsForUpdatedAlert = this.getContentsForUpdatedAlert.bind(this);
   }
 
   refreshPage() {
     window.location.reload();
   }
 
-  renderSomeoneEditingAlert() {
+  getContentsForSomeoneEditingAlert() {
     const { t } = this.props;
-    return (
-      <div className="card grw-page-status-alert text-white bg-success d-hackmd-none fixed-bottom">
-        <div className="card-body">
-          <p className="card-text grw-card-label-container">
-            <i className="icon-fw icon-people"></i>
-            {t('hackmd.someone_editing')}
-          </p>
-          <p className="card-text grw-card-btn-container">
-            <a href="#hackmd" className="btn btn-outline-white">
-              <i className="fa fa-fw fa-file-text-o"></i>
-              Open HackMD Editor
-            </a>
-          </p>
-        </div>
-      </div>
-    );
+    return [
+      ['bg-success', 'd-hackmd-none'],
+      <>
+        <i className="icon-fw icon-people"></i>
+        {t('hackmd.someone_editing')}
+      </>,
+      <a href="#hackmd" className="btn btn-outline-white">
+        <i className="fa fa-fw fa-file-text-o mr-1"></i>
+        Open HackMD Editor
+      </a>,
+    ];
   }
 
-  renderDraftExistsAlert(isRealtime) {
+  getContentsForDraftExistsAlert(isRealtime) {
     const { t } = this.props;
-    return (
-      <div className="card grw-page-status-alert text-white bg-success d-hackmd-none fixed-bottom">
-        <div className="card-body">
-          <p className="card-text grw-card-label-container">
-            <i className="icon-fw icon-pencil"></i>
-            {t('hackmd.this_page_has_draft')}
-          </p>
-          <p className="card-text grw-card-btn-container">
-            <a href="#hackmd" className="btn btn-outline-white">
-              <i className="fa fa-fw fa-file-text-o"></i>
-              Open HackMD Editor
-            </a>
-          </p>
-        </div>
-      </div>
-    );
+    return [
+      ['bg-success', 'd-hackmd-none'],
+      <>
+        <i className="icon-fw icon-pencil"></i>
+        {t('hackmd.this_page_has_draft')}
+      </>,
+      <a href="#hackmd" className="btn btn-outline-white">
+        <i className="fa fa-fw fa-file-text-o mr-1"></i>
+        Open HackMD Editor
+      </a>,
+    ];
   }
 
-  renderUpdatedAlert() {
+  getContentsForUpdatedAlert() {
     const { t } = this.props;
     const label1 = t('edited this page');
     const label2 = t('Load latest');
 
-    return (
-      <div className="card grw-page-status-alert text-white bg-warning fixed-bottom">
-        <div className="card-body">
-          <p className="card-text grw-card-label-container">
-            <i className="icon-fw icon-bulb"></i>
-            {this.props.pageContainer.state.lastUpdateUsername} {label1}
-          </p>
-          <p className="card-text grw-card-btn-container">
-            <a href="#" className="btn btn-outline-white" onClick={this.refreshPage}>
-              <i className="icon-fw icon-reload"></i>
-              {label2}
-            </a>
-          </p>
-        </div>
-      </div>
-    );
+    return [
+      ['bg-warning'],
+      <>
+        <i className="icon-fw icon-bulb"></i>
+        {this.props.pageContainer.state.lastUpdateUsername} {label1}
+      </>,
+      <a href="#" className="btn btn-outline-white" onClick={this.refreshPage}>
+        <i className="icon-fw icon-reload mr-1"></i>
+        {label2}
+      </a>,
+    ];
   }
 
   render() {
-    let content = <React.Fragment></React.Fragment>;
-
     const {
       revisionId, revisionIdHackmdSynced, remoteRevisionId, hasDraftOnHackmd, isHackmdDraftUpdatingInRealtime,
     } = this.props.pageContainer.state;
@@ -107,22 +90,39 @@ class PageStatusAlert extends React.Component {
     const isRevisionOutdated = revisionId !== remoteRevisionId;
     const isHackmdDocumentOutdated = revisionIdHackmdSynced !== remoteRevisionId;
 
+    let getContentsFunc = null;
+
     // when remote revision is newer than both
     if (isHackmdDocumentOutdated && isRevisionOutdated) {
-      content = this.renderUpdatedAlert();
+      getContentsFunc = this.getContentsFunc;
     }
     // when someone editing with HackMD
     else if (isHackmdDraftUpdatingInRealtime) {
-      content = this.renderSomeoneEditingAlert();
+      getContentsFunc = this.getContentsForSomeoneEditingAlert;
     }
     // when the draft of HackMD is newest
     else if (hasDraftOnHackmd) {
-      content = this.renderDraftExistsAlert();
+      getContentsFunc = this.getContentsForDraftExistsAlert;
+    }
+    // do not render anything
+    else {
+      return null;
     }
 
-    content = this.renderUpdatedAlert();
+    const [additionalClasses, label, btn] = getContentsFunc();
 
-    return content;
+    return (
+      <div className={`card grw-page-status-alert text-white fixed-bottom animated fadeInUp faster ${additionalClasses.join(' ')}`}>
+        <div className="card-body">
+          <p className="card-text grw-card-label-container">
+            {label}
+          </p>
+          <p className="card-text grw-card-btn-container">
+            {btn}
+          </p>
+        </div>
+      </div>
+    );
   }
 
 }

+ 106 - 2
src/client/js/services/AdminOidcSecurityContainer.js

@@ -23,6 +23,14 @@ export default class AdminOidcSecurityContainer extends Container {
       callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/oidc/callback'),
       oidcProviderName: '',
       oidcIssuerHost: '',
+      oidcAuthorizationEndpoint: '',
+      oidcTokenEndpoint: '',
+      oidcRevocationEndpoint: '',
+      oidcIntrospectionEndpoint: '',
+      oidcUserInfoEndpoint: '',
+      oidcEndSessionEndpoint: '',
+      oidcRegistrationEndpoint: '',
+      oidcJWKSUri: '',
       oidcClientId: '',
       oidcClientSecret: '',
       oidcAttrMapId: '',
@@ -45,6 +53,14 @@ export default class AdminOidcSecurityContainer extends Container {
       this.setState({
         oidcProviderName: oidcAuth.oidcProviderName,
         oidcIssuerHost: oidcAuth.oidcIssuerHost,
+        oidcAuthorizationEndpoint: oidcAuth.oidcAuthorizationEndpoint,
+        oidcTokenEndpoint: oidcAuth.oidcTokenEndpoint,
+        oidcRevocationEndpoint: oidcAuth.oidcRevocationEndpoint,
+        oidcIntrospectionEndpoint: oidcAuth.oidcIntrospectionEndpoint,
+        oidcUserInfoEndpoint: oidcAuth.oidcUserInfoEndpoint,
+        oidcEndSessionEndpoint: oidcAuth.oidcEndSessionEndpoint,
+        oidcRegistrationEndpoint: oidcAuth.oidcRegistrationEndpoint,
+        oidcJWKSUri: oidcAuth.oidcJWKSUri,
         oidcClientId: oidcAuth.oidcClientId,
         oidcClientSecret: oidcAuth.oidcClientSecret,
         oidcAttrMapId: oidcAuth.oidcAttrMapId,
@@ -83,6 +99,62 @@ export default class AdminOidcSecurityContainer extends Container {
     this.setState({ oidcIssuerHost: inputValue });
   }
 
+  /**
+   * Change oidcAuthorizationEndpoint
+   */
+  changeOidcAuthorizationEndpoint(inputValue) {
+    this.setState({ oidcAuthorizationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcTokenEndpoint
+   */
+  changeOidcTokenEndpoint(inputValue) {
+    this.setState({ oidcTokenEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcRevocationEndpoint
+   */
+  changeOidcRevocationEndpoint(inputValue) {
+    this.setState({ oidcRevocationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcIntrospectionEndpoint
+   */
+  changeOidcIntrospectionEndpoint(inputValue) {
+    this.setState({ oidcIntrospectionEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcUserInfoEndpoint
+   */
+  changeOidcUserInfoEndpoint(inputValue) {
+    this.setState({ oidcUserInfoEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcEndSessionEndpoint
+   */
+  changeOidcEndSessionEndpoint(inputValue) {
+    this.setState({ oidcEndSessionEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcRegistrationEndpoint
+   */
+  changeOidcRegistrationEndpoint(inputValue) {
+    this.setState({ oidcRegistrationEndpoint: inputValue });
+  }
+
+  /**
+   * Change oidcJWKSUri
+   */
+  changeOidcJWKSUri(inputValue) {
+    this.setState({ oidcJWKSUri: inputValue });
+  }
+
   /**
    * Change oidcClientId
    */
@@ -144,13 +216,37 @@ export default class AdminOidcSecurityContainer extends Container {
    */
   async updateOidcSetting() {
     const {
-      oidcProviderName, oidcIssuerHost, oidcClientId, oidcClientSecret, oidcAttrMapId, oidcAttrMapUserName,
-      oidcAttrMapName, oidcAttrMapEmail, isSameUsernameTreatedAsIdenticalUser, isSameEmailTreatedAsIdenticalUser,
+      oidcProviderName,
+      oidcIssuerHost,
+      oidcAuthorizationEndpoint,
+      oidcTokenEndpoint,
+      oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint,
+      oidcJWKSUri,
+      oidcClientId,
+      oidcClientSecret,
+      oidcAttrMapId,
+      oidcAttrMapUserName,
+      oidcAttrMapName,
+      oidcAttrMapEmail,
+      isSameUsernameTreatedAsIdenticalUser,
+      isSameEmailTreatedAsIdenticalUser,
     } = this.state;
 
     let requestParams = {
       oidcProviderName,
       oidcIssuerHost,
+      oidcAuthorizationEndpoint,
+      oidcTokenEndpoint,
+      oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint,
+      oidcJWKSUri,
       oidcClientId,
       oidcClientSecret,
       oidcAttrMapId,
@@ -168,6 +264,14 @@ export default class AdminOidcSecurityContainer extends Container {
     this.setState({
       oidcProviderName: securitySettingParams.oidcProviderName,
       oidcIssuerHost: securitySettingParams.oidcIssuerHost,
+      oidcAuthorizationEndpoint: securitySettingParams.oidcAuthorizationEndpoint,
+      oidcTokenEndpoint: securitySettingParams.oidcTokenEndpoint,
+      oidcRevocationEndpoint: securitySettingParams.oidcRevocationEndpoint,
+      oidcIntrospectionEndpoint: securitySettingParams.oidcIntrospectionEndpoint,
+      oidcUserInfoEndpoint: securitySettingParams.oidcUserInfoEndpoint,
+      oidcEndSessionEndpoint: securitySettingParams.oidcEndSessionEndpoint,
+      oidcRegistrationEndpoint: securitySettingParams.oidcRegistrationEndpoint,
+      oidcJWKSUri: securitySettingParams.oidcJWKSUri,
       oidcClientId: securitySettingParams.oidcClientId,
       oidcClientSecret: securitySettingParams.oidcClientSecret,
       oidcAttrMapId: securitySettingParams.oidcAttrMapId,

+ 11 - 8
src/client/js/services/EditorContainer.js

@@ -57,16 +57,19 @@ export default class EditorContainer extends Container {
    * initialize state for page permission
    */
   initStateGrant() {
-    const elem = document.getElementById('save-page-controls');
+    const mainContent = document.getElementById('content-main');
 
-    if (elem) {
-      this.state.grant = +elem.dataset.grant;
+    if (mainContent == null) {
+      logger.debug('#content-main element is not exists');
+      return;
+    }
 
-      const grantGroupId = elem.dataset.grantGroup;
-      if (grantGroupId != null && grantGroupId.length > 0) {
-        this.state.grantGroupId = grantGroupId;
-        this.state.grantGroupName = elem.dataset.grantGroupName;
-      }
+    this.state.grant = +mainContent.getAttribute('data-page-grant');
+
+    const grantGroupId = mainContent.getAttribute('data-page-grant-group');
+    if (grantGroupId != null && grantGroupId.length > 0) {
+      this.state.grantGroupId = grantGroupId;
+      this.state.grantGroupName = mainContent.getAttribute('data-page-grant-group-name');
     }
   }
 

+ 2 - 2
src/client/styles/scss/_sidebar.scss

@@ -152,7 +152,7 @@
 
 // Drawer Mode
 @mixin drawer() {
-  z-index: $zindex-fixed + 1;
+  z-index: $zindex-fixed + 2;
 
   // override @atlaskit/navigation-next styles
   div[data-testid='Navigation'] {
@@ -230,5 +230,5 @@
 }
 
 .grw-sidebar-backdrop.modal-backdrop {
-  z-index: $zindex-fixed - 4;
+  z-index: $zindex-fixed + 1;
 }

+ 5 - 1
src/client/styles/scss/_subnav.scss

@@ -69,7 +69,11 @@
    * Compact Mode
    */
   &.grw-subnav-compact {
-    min-height: 90px;
+    min-height: 70px;
+
+    @include media-breakpoint-up(md) {
+      min-height: 90px;
+    }
 
     .btn-like,
     .btn-bookmark {

+ 56 - 0
src/server/routes/apiv3/security-setting.js

@@ -65,6 +65,14 @@ const validator = {
   oidcAuth: [
     body('oidcProviderName').if(value => value != null).isString(),
     body('oidcIssuerHost').if(value => value != null).isString(),
+    body('oidcAuthorizationEndpoint').if(value => value != null).isString(),
+    body('oidcTokenEndpoint').if(value => value != null).isString(),
+    body('oidcRevocationEndpoint').if(value => value != null).isString(),
+    body('oidcIntrospectionEndpoint').if(value => value != null).isString(),
+    body('oidcUserInfoEndpoint').if(value => value != null).isString(),
+    body('oidcEndSessionEndpoint').if(value => value != null).isString(),
+    body('oidcRegistrationEndpoint').if(value => value != null).isString(),
+    body('oidcJWKSUri').if(value => value != null).isString(),
     body('oidcClientId').if(value => value != null).isString(),
     body('oidcClientSecret').if(value => value != null).isString(),
     body('oidcAttrMapId').if(value => value != null).isString(),
@@ -219,6 +227,30 @@ const validator = {
  *          oidcIssuerHost:
  *            type: string
  *            description: issuer host for oidc
+ *          oidcAuthorizationEndpoint:
+ *            type: string
+ *            description: authorization endpoint for oidc
+ *          oidcTokenEndpoint:
+ *            type: string
+ *            description: token endpoint for oidc
+ *          oidcRevocationEndpoint:
+ *            type: string
+ *            description: revocation endpoint for oidc
+ *          oidcIntrospectionEndpoint:
+ *            type: string
+ *            description: introspection endpoint for oidc
+ *          oidcUserInfoEndpoint:
+ *            type: string
+ *            description: userinfo endpoint for oidc
+ *          oidcEndSessionEndpoint:
+ *            type: string
+ *            description: end session endpoint for oidc
+ *          oidcRegistrationEndpoint:
+ *            type: string
+ *            description: registration endpoint for oidc
+ *          oidcJWKSUri:
+ *            type: string
+ *            description: JSON Web Key Set URI for oidc
  *          oidcClientId:
  *            type: string
  *            description: client id for oidc
@@ -376,6 +408,14 @@ module.exports = (crowi) => {
       oidcAuth: {
         oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
         oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
         oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
         oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
         oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),
@@ -767,6 +807,14 @@ module.exports = (crowi) => {
     const requestParams = {
       'security:passport-oidc:providerName': req.body.oidcProviderName,
       'security:passport-oidc:issuerHost': req.body.oidcIssuerHost,
+      'security:passport-oidc:authorizationEndpoint': req.body.oidcAuthorizationEndpoint,
+      'security:passport-oidc:tokenEndpoint': req.body.oidcTokenEndpoint,
+      'security:passport-oidc:revocationEndpoint': req.body.oidcRevocationEndpoint,
+      'security:passport-oidc:introspectionEndpoint': req.body.oidcIntrospectionEndpoint,
+      'security:passport-oidc:userInfoEndpoint': req.body.oidcUserInfoEndpoint,
+      'security:passport-oidc:endSessionEndpoint': req.body.oidcEndSessionEndpoint,
+      'security:passport-oidc:registrationEndpoint': req.body.oidcRegistrationEndpoint,
+      'security:passport-oidc:jwksUri': req.body.oidcJWKSUri,
       'security:passport-oidc:clientId': req.body.oidcClientId,
       'security:passport-oidc:clientSecret': req.body.oidcClientSecret,
       'security:passport-oidc:attrMapId': req.body.oidcAttrMapId,
@@ -783,6 +831,14 @@ module.exports = (crowi) => {
       const securitySettingParams = {
         oidcProviderName: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:providerName'),
         oidcIssuerHost: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:issuerHost'),
+        oidcAuthorizationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint'),
+        oidcTokenEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint'),
+        oidcRevocationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint'),
+        oidcIntrospectionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint'),
+        oidcUserInfoEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint'),
+        oidcEndSessionEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint'),
+        oidcRegistrationEndpoint: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint'),
+        oidcJWKSUri: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:jwksUri'),
         oidcClientId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientId'),
         oidcClientSecret: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:clientSecret'),
         oidcAttrMapId: await crowi.configManager.getConfig('crowi', 'security:passport-oidc:attrMapId'),

+ 4 - 0
src/server/routes/attachment.js

@@ -193,6 +193,10 @@ module.exports = function(crowi, app) {
       return res.sendStatus(304);
     }
 
+    if (fileUploader.canRespond()) {
+      return fileUploader.respond(res, attachment);
+    }
+
     let fileStream;
     try {
       fileStream = await fileUploader.findDeliveryFile(attachment);

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

@@ -137,6 +137,18 @@ const ENV_VAR_NAME_TO_CONFIG_INFO = {
     type:    TYPES.BOOLEAN,
     default: false,
   },
+  FILE_UPLOAD_LOCAL_USE_INTERNAL_REDIRECT: {
+    ns:      'crowi',
+    key:     'fileUpload:local:useInternalRedirect',
+    type:    TYPES.BOOLEAN,
+    default: false,
+  },
+  FILE_UPLOAD_LOCAL_INTERNAL_REDIRECT_PATH: {
+    ns:      'crowi',
+    key:     'fileUpload:local:internalRedirectPath',
+    type:    TYPES.STRING,
+    default: '/growi-internal/',
+  },
   ELASTICSEARCH_URI: {
     ns:      'crowi',
     key:     'app:elasticsearchUri',

+ 25 - 0
src/server/service/file-uploader/local.js

@@ -4,6 +4,7 @@ const fs = require('fs');
 const path = require('path');
 const mkdir = require('mkdirp');
 const streamToPromise = require('stream-to-promise');
+const urljoin = require('url-join');
 
 module.exports = function(crowi) {
   const Uploader = require('./uploader');
@@ -92,5 +93,29 @@ module.exports = function(crowi) {
     return lib.doCheckLimit(uploadFileSize, maxFileSize, totalLimit);
   };
 
+  /**
+   * Checks if Uploader can respond to the HTTP request.
+   */
+  lib.canRespond = () => {
+    // Check whether to use internal redirect of nginx or Apache.
+    return process.env.FILE_UPLOAD === 'local' && lib.configManager.getConfig('crowi', 'fileUpload:local:useInternalRedirect');
+  };
+
+  /**
+   * Respond to the HTTP request.
+   * @param {Response} res
+   * @param {Response} attachment
+   */
+  lib.respond = (res, attachment) => {
+    // Responce using internal redirect of nginx or Apache.
+    const storagePath = getFilePathOnStorage(attachment);
+    const relativePath = path.relative(crowi.publicDir, storagePath);
+    const internalPathRoot = lib.configManager.getConfig('crowi', 'fileUpload:local:internalRedirectPath');
+    const internalPath = urljoin(internalPathRoot, relativePath);
+    res.set('X-Accel-Redirect', internalPath);
+    res.set('X-Sendfile', storagePath);
+    return res.end();
+  };
+
   return lib;
 };

+ 16 - 0
src/server/service/file-uploader/uploader.js

@@ -54,6 +54,22 @@ class Uploader {
 
   }
 
+  /**
+   * Checks if Uploader can respond to the HTTP request.
+   */
+  canRespond() {
+    return false;
+  }
+
+  /**
+   * Respond to the HTTP request.
+   * @param {Response} res
+   * @param {Response} attachment
+   */
+  respond(res, attachment) {
+    throw new Error('Implement this');
+  }
+
 }
 
 module.exports = Uploader;

+ 34 - 0
src/server/service/passport.js

@@ -570,6 +570,40 @@ class PassportService {
     const oidcIssuer = await OIDCIssuer.discover(issuerHost);
     debug('Discovered issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
 
+    const authorizationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:authorizationEndpoint');
+    if (authorizationEndpoint) {
+      oidcIssuer.metadata.authorization_endpoint = authorizationEndpoint;
+    }
+    const tokenEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:tokenEndpoint');
+    if (tokenEndpoint) {
+      oidcIssuer.metadata.token_endpoint = tokenEndpoint;
+    }
+    const revocationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:revocationEndpoint');
+    if (revocationEndpoint) {
+      oidcIssuer.metadata.revocation_endpoint = revocationEndpoint;
+    }
+    const introspectionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:introspectionEndpoint');
+    if (introspectionEndpoint) {
+      oidcIssuer.metadata.introspection_endpoint = introspectionEndpoint;
+    }
+    const userInfoEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:userInfoEndpoint');
+    if (userInfoEndpoint) {
+      oidcIssuer.metadata.userinfo_endpoint = userInfoEndpoint;
+    }
+    const endSessionEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:endSessionEndpoint');
+    if (endSessionEndpoint) {
+      oidcIssuer.metadata.end_session_endpoint = endSessionEndpoint;
+    }
+    const registrationEndpoint = configManager.getConfig('crowi', 'security:passport-oidc:registrationEndpoint');
+    if (registrationEndpoint) {
+      oidcIssuer.metadata.registration_endpoint = registrationEndpoint;
+    }
+    const jwksUri = configManager.getConfig('crowi', 'security:passport-oidc:jwksUri');
+    if (jwksUri) {
+      oidcIssuer.metadata.jwks_uri = jwksUri;
+    }
+    debug('Configured issuer %s %O', oidcIssuer.issuer, oidcIssuer.metadata);
+
     const client = new oidcIssuer.Client({
       client_id: clientId,
       client_secret: clientSecret,

+ 0 - 15
src/server/views/_form.html

@@ -10,20 +10,5 @@
 {% endif %}
 
 <div id="page-editor-navbar-bottom-container" class="d-none d-edit-block"></div>
-{#
-<div class="page-editor-footer d-flex flex-row align-items-center justify-content-between">
-
-  <div>
-    <div id="page-editor-options-selector" class="d-none d-md-block"></div>
-  </div>
-
-  <div id="save-page-controls"
-    data-grant="{{ grant }}"
-    data-grant-group="{{ grantedGroupId }}"
-    data-grant-group-name="{{ grantedGroupName }}">
-  </div>
-
-</div>
-#}
 
 <div class="file-module hidden"></div>

+ 3 - 0
src/server/views/widget/page_content.html

@@ -9,6 +9,9 @@
   data-page-revision-id-hackmd-synced="{% if revisionHackmdSynced %}{{ revisionHackmdSynced.toString() }}{% endif %}"
   data-page-id-on-hackmd="{% if pageIdOnHackmd %}{{ pageIdOnHackmd.toString() }}{% endif %}"
   data-page-has-draft-on-hackmd="{% if hasDraftOnHackmd %}{{ hasDraftOnHackmd.toString() }}{% endif %}"
+  data-page-grant="{{ grant }}"
+  data-page-grant-group="{{ grantedGroupId }}"
+  data-page-grant-group-name="{{ grantedGroupName }}"
   data-page-is-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-forbidden="{% if forbidden %}true{% else %}false{% endif %}"