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

Merge branch 'master' into reactify/personal-settings

itizawa 6 лет назад
Родитель
Сommit
2ec1a0b446
41 измененных файлов с 639 добавлено и 983 удалено
  1. 4 0
      CHANGES.md
  2. 1 1
      config/env.prod.js
  3. 3 0
      config/logger/config.prod.js
  4. 2 2
      package.json
  5. 7 0
      resource/locales/en-US/sandbox-diagrams.md
  6. 71 0
      resource/locales/en-US/sandbox-math.md
  7. 22 358
      resource/locales/en-US/sandbox.md
  8. 5 3
      resource/locales/en-US/translation.json
  9. 7 0
      resource/locales/ja/sandbox-diagrams.md
  10. 71 0
      resource/locales/ja/sandbox-math.md
  11. 22 357
      resource/locales/ja/sandbox.md
  12. 4 2
      resource/locales/ja/translation.json
  13. 19 12
      src/client/js/components/Admin/Security/BasicSecuritySetting.jsx
  14. 20 12
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  15. 25 12
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  16. 24 13
      src/client/js/components/Admin/Security/LdapSecuritySetting.jsx
  17. 30 14
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  18. 24 11
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  19. 26 17
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  20. 12 14
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  21. 25 12
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  22. 2 1
      src/client/js/components/Drawio.jsx
  23. 7 7
      src/client/js/components/Page.jsx
  24. 2 2
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  25. 29 45
      src/client/js/components/PageEditor/DrawioModal.jsx
  26. 25 14
      src/client/js/services/AdminGeneralSecurityContainer.js
  27. 3 2
      src/client/js/services/AdminLocalSecurityContainer.js
  28. 2 0
      src/client/js/services/AdminSamlSecurityContainer.js
  29. 2 2
      src/client/js/services/AppContainer.js
  30. 3 0
      src/client/styles/scss/_drawio.scss
  31. 4 17
      src/client/styles/scss/_handsontable.scss
  32. 26 0
      src/client/styles/scss/_mixins.scss
  33. 1 0
      src/client/styles/scss/style-app.scss
  34. 3 0
      src/lib/service/logger/stream.prod.js
  35. 2 2
      src/server/crowi/express-init.js
  36. 45 9
      src/server/routes/apiv3/security-setting.js
  37. 12 12
      src/server/routes/installer.js
  38. 2 1
      src/server/routes/login.js
  39. 14 6
      src/server/service/passport.js
  40. 8 8
      src/test/middleware/safe-redirect.test.js
  41. 23 15
      yarn.lock

+ 4 - 0
CHANGES.md

@@ -6,6 +6,10 @@
 * Feature: SAML Attribute-based Login Control
 * Improvement: Reactify admin pages (Security)
 
+## v3.6.10
+
+* Fix: Redirect logic for users except for actives
+    * Introduced by 3.6.9
 
 ## v3.6.9
 

+ 1 - 1
config/env.prod.js

@@ -1,4 +1,4 @@
 module.exports = {
   NODE_ENV: 'production',
-  // FORMAT_NODE_LOG: false,
+  // FORMAT_NODE_LOG: false, // default: true
 };

+ 3 - 0
config/logger/config.prod.js

@@ -1,3 +1,6 @@
 module.exports = {
   default: 'info',
+
+  'growi:routes:login-passport': 'debug',
+  'growi:service:PassportService': 'debug',
 };

+ 2 - 2
package.json

@@ -194,8 +194,8 @@
     "load-css-file": "^1.0.0",
     "lodash-webpack-plugin": "^0.11.5",
     "markdown-it": "^10.0.0",
-    "markdown-it-blockdiag": "^1.0.2",
-    "markdown-it-drawio-viewer": "^1.1.0",
+    "markdown-it-blockdiag": "^1.0.3",
+    "markdown-it-drawio-viewer": "^1.1.3",
     "markdown-it-emoji": "^1.4.0",
     "markdown-it-footnote": "^3.0.1",
     "markdown-it-mathjax": "^2.0.0",

Разница между файлами не показана из-за своего большого размера
+ 7 - 0
resource/locales/en-US/sandbox-diagrams.md


+ 71 - 0
resource/locales/en-US/sandbox-math.md

@@ -0,0 +1,71 @@
+# :pencil: Math
+
+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
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$

+ 22 - 358
resource/locales/en-US/sandbox.md

@@ -28,8 +28,11 @@
 ```
 
 ### Header 3
+
 #### Header 4
+
 ##### Header 5
+
 ###### Header 6
 
 ## Block 段落
@@ -48,7 +51,7 @@
 
 ## Br 改行
 
-改行の前に半角スペース`  `を2つ記述します。
+改行の前に半角スペース``を2つ記述します。
 ***この挙動は、オプションで変更可能です***
 
 ```
@@ -159,6 +162,7 @@ ___
 # :pencil: Typography
 
 ## 強調
+
 ### em
 
 アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
@@ -353,30 +357,30 @@ aligned    | aligned     | aligned
 
 ```
 ::: tsv
-Content Cell 	Content Cell
-Content Cell 	Content Cell
+Content Cell  Content Cell
+Content Cell  Content Cell
 :::
 ```
 
 ::: tsv
-Content Cell	Content Cell
-Content Cell	Content Cell
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## TSV ヘッダ付き (crowi-plus 独自記法)
 
 ```
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 ```
 
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## CSV (crowi-plus 独自記法)
@@ -442,351 +446,11 @@ See [emojione](https://www.emojione.com/)
 :watch: :gear: :gem: :wrench: :envelope:
 
 
-# :pencil: Math
-
-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
-  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
-
-## The Lorenz Equations
-
-$$
-\begin{align}
-\dot{x} & = \sigma(y-x) \\
-\dot{y} & = \rho x - y - xz \\
-\dot{z} & = -\beta z + xy
-\end{align}
-$$
-
-
-## The Cauchy-Schwarz Inequality
-
-$$
-\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
- \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
-$$
-
-## A Cross Product Formula
-
-$$
-\mathbf{V}_1 \times \mathbf{V}_2 =
- \begin{vmatrix}
-  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
-  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
-  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
- \end{vmatrix}
-$$
-
-
-## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
-
-$$
-P(E) = {n \choose k} p^k (1-p)^{ n-k}
-$$
-
-## An Identity of Ramanujan
-
-$$
-\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
-     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
-      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
-$$
-
-## A Rogers-Ramanujan Identity
-
-$$
-1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
-    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
-     \quad\quad \text{for $|q|<1$}.
-$$
-
-## Maxwell's Equations
-
-$$
-\begin{align}
-  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
-  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
-  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
-  \nabla \cdot \vec{\mathbf{B}} & = 0
-\end{align}
-$$
-
-<!-- Reset MathJax -->
-<div class="clearfix"></div>
-
-
-# :pencil: UML Diagrams
-
-See [PlantUML](http://plantuml.com/).
-
-## シーケンス図
-
-@startuml
-skinparam sequenceArrowThickness 2
-skinparam roundcorner 20
-skinparam maxmessagesize 60
-skinparam sequenceParticipant underline
-
-actor User
-participant "First Class" as A
-participant "Second Class" as B
-participant "Last Class" as C
-
-User -> A: DoWork
-activate A
-
-A -> B: Create Request
-activate B
+# :heavy_plus_sign: More..
 
-B -> C: DoWork
-activate C
-C --> B: WorkDone
-destroy C
-
-B --> A: Request Created
-deactivate B
-
-A --> User: Done
-deactivate A
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## クラス図
-
-@startuml
-
-class BaseClass
-
-namespace net.dummy #DDDDDD {
-    .BaseClass <|-- Person
-    Meeting o-- Person
-
-    .BaseClass <|- Meeting
-}
-
-namespace net.foo {
-  net.dummy.Person  <|- Person
-  .BaseClass <|-- Person
-
-  net.dummy.Meeting o-- Person
-}
-
-BaseClass <|-- net.unused.Person
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## コンポーネント図
-
-@startuml
-
-package "Some Group" {
-  HTTP - [First Component]
-  [Another Component]
-}
-
-node "Other Groups" {
-  FTP - [Second Component]
-  [First Component] --> FTP
-}
-
-cloud {
-  [Example 1]
-}
-
-
-database "MySql" {
-  folder "This is my folder" {
-    [Folder 3]
-  }
-  frame "Foo" {
-    [Frame 4]
-  }
-}
-
-
-[Another Component] --> [Example 1]
-[Example 1] --> [Folder 3]
-[Folder 3] --> [Frame 4]
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## ステート図
-
-
-@startuml
-scale 600 width
-
-[*] -> State1
-State1 --> State2 : Succeeded
-State1 --> [*] : Aborted
-State2 --> State3 : Succeeded
-State2 --> [*] : Aborted
-state State3 {
-  state "Accumulate Enough Data\nLong State Name" as long1
-  long1 : Just a test
-  [*] --> long1
-  long1 --> long1 : New Data
-  long1 --> ProcessData : Enough Data
-}
-State3 --> State3 : Failed
-State3 --> [*] : Succeeded / Save Result
-State3 --> [*] : Aborted
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-# :pencil: blockdiag
-
-See [blockdiag](http://blockdiag.com/).
-
-## blockdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
-
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
-
-</div>
+- Try to attach Bootstrap3 Tags?
+    - :arrow_right: [/Sandbox/Bootstrap3]
+- Try to draw Diagrams?
+    - :arrow_right: [/Sandbox/Diagrams]
+- Try to write Math Formulas?
+    - :arrow_right: [/Sandbox/Math]

+ 5 - 3
resource/locales/en-US/translation.json

@@ -391,6 +391,7 @@
     "admin_and_author": "Admin and Author",
     "anyone": "Anyone",
     "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}}",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting_link": "Go to Markdown settings",
@@ -423,6 +424,7 @@
     "missing mandatory configs": "The following mandatory items are not set in either database nor environment variables.",
     "Local": {
       "name": "ID/Password",
+      "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}/code> .",
       "enable_local": "enable ID/Password"
     },
     "ldap": {
@@ -466,10 +468,10 @@
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
       "cert_detail": "PEM-encoded X.509 signing certificate to validate the response from IdP",
       "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
-      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>%s</code> .",
+      "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
       "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-      "attr_based_login_control_rule_detail": "A rule is written in the form of concatenating <code>attribute name = value</code> with <code>|</code> and <code>&</code>. The or operator (|) has a lower precedence than the and operator (&). If operator precedence is equal, left to right associativity is used.",
-      "attr_based_login_control_rule_example": "For example, if a rule is <code>Department = A | Department = B & Position = Leader</code>, users with <code>Department</code> as <code>A</code> or users with <code>Department</code> as <code>B</code> and <code>Position</code> as <code>Leader</code> <strong>can</strong> sign in.",
+      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>Supported Queries:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>Unsupported Queries:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
+      "attr_based_login_control_rule_example": "<h6>Example</h6>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
       "updated_saml": "Succeeded to update SAML setting"
     },
     "Basic": {

Разница между файлами не показана из-за своего большого размера
+ 7 - 0
resource/locales/ja/sandbox-diagrams.md


+ 71 - 0
resource/locales/ja/sandbox-math.md

@@ -0,0 +1,71 @@
+# :pencil: Math
+
+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
+  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
+
+## The Lorenz Equations
+
+$$
+\begin{align}
+\dot{x} & = \sigma(y-x) \\
+\dot{y} & = \rho x - y - xz \\
+\dot{z} & = -\beta z + xy
+\end{align}
+$$
+
+
+## The Cauchy-Schwarz Inequality
+
+$$
+\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
+ \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
+$$
+
+## A Cross Product Formula
+
+$$
+\mathbf{V}_1 \times \mathbf{V}_2 =
+ \begin{vmatrix}
+  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
+  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
+  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
+ \end{vmatrix}
+$$
+
+
+## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
+
+$$
+P(E) = {n \choose k} p^k (1-p)^{ n-k}
+$$
+
+## An Identity of Ramanujan
+
+$$
+\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
+     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
+      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
+$$
+
+## A Rogers-Ramanujan Identity
+
+$$
+1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
+    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
+     \quad\quad \text{for $|q|<1$}.
+$$
+
+## Maxwell's Equations
+
+$$
+\begin{align}
+  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
+  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
+  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
+  \nabla \cdot \vec{\mathbf{B}} & = 0
+\end{align}
+$$

+ 22 - 357
resource/locales/ja/sandbox.md

@@ -28,8 +28,11 @@
 ```
 
 ### 見出し3
+
 #### 見出し4
+
 ##### 見出し5
+
 ###### 見出し6
 
 ## Block 段落
@@ -48,7 +51,7 @@
 
 ## Br 改行
 
-改行の前に半角スペース`  `を2つ記述します。
+改行の前に半角スペース``を2つ記述します。
 ***この挙動は、オプションで変更可能です***
 
 ```
@@ -159,6 +162,7 @@ ___
 # :pencil: Typography
 
 ## 強調
+
 ### em
 
 アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
@@ -353,30 +357,30 @@ aligned    | aligned     | aligned
 
 ```
 ::: tsv
-Content Cell 	Content Cell
-Content Cell 	Content Cell
+Content Cell  Content Cell
+Content Cell  Content Cell
 :::
 ```
 
 ::: tsv
-Content Cell	Content Cell
-Content Cell	Content Cell
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## TSV ヘッダ付き (crowi-plus 独自記法)
 
 ```
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 ```
 
 ::: tsv-h
-First Header	Second Header
-Content Cell	Content Cell
-Content Cell	Content Cell
+First Header Second Header
+Content Cell Content Cell
+Content Cell Content Cell
 :::
 
 ## CSV (crowi-plus 独自記法)
@@ -442,351 +446,12 @@ See [emojione](https://www.emojione.com/)
 :watch: :gear: :gem: :wrench: :envelope:
 
 
-# :pencil: Math
-
-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
-  $$x = {-b \pm \sqrt{b^2-4ac} \over 2a}.$$
-
-## The Lorenz Equations
-
-$$
-\begin{align}
-\dot{x} & = \sigma(y-x) \\
-\dot{y} & = \rho x - y - xz \\
-\dot{z} & = -\beta z + xy
-\end{align}
-$$
-
-
-## The Cauchy-Schwarz Inequality
-
-$$
-\left( \sum_{k=1}^n a_k b_k \right)^{\!\!2} \leq
- \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)
-$$
-
-## A Cross Product Formula
-
-$$
-\mathbf{V}_1 \times \mathbf{V}_2 =
- \begin{vmatrix}
-  \mathbf{i} & \mathbf{j} & \mathbf{k} \\
-  \frac{\partial X}{\partial u} & \frac{\partial Y}{\partial u} & 0 \\
-  \frac{\partial X}{\partial v} & \frac{\partial Y}{\partial v} & 0 \\
- \end{vmatrix}
-$$
-
-
-## The probability of getting $\left(k\right)$ heads when flipping $\left(n\right)$ coins is:
-
-$$
-P(E) = {n \choose k} p^k (1-p)^{ n-k}
-$$
-
-## An Identity of Ramanujan
-
-$$
-\frac{1}{(\sqrt{\phi \sqrt{5}}-\phi) e^{\frac25 \pi}} =
-     1+\frac{e^{-2\pi}} {1+\frac{e^{-4\pi}} {1+\frac{e^{-6\pi}}
-      {1+\frac{e^{-8\pi}} {1+\ldots} } } }
-$$
-
-## A Rogers-Ramanujan Identity
-
-$$
-1 +  \frac{q^2}{(1-q)}+\frac{q^6}{(1-q)(1-q^2)}+\cdots =
-    \prod_{j=0}^{\infty}\frac{1}{(1-q^{5j+2})(1-q^{5j+3})},
-     \quad\quad \text{for $|q|<1$}.
-$$
-
-## Maxwell's Equations
-
-$$
-\begin{align}
-  \nabla \times \vec{\mathbf{B}} -\, \frac1c\, \frac{\partial\vec{\mathbf{E}}}{\partial t} & = \frac{4\pi}{c}\vec{\mathbf{j}} \\
-  \nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
-  \nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
-  \nabla \cdot \vec{\mathbf{B}} & = 0
-\end{align}
-$$
-
-<!-- Reset MathJax -->
-<div class="clearfix"></div>
-
-
-# :pencil: UML Diagrams
-
-See [PlantUML](http://plantuml.com/).
-
-## シーケンス図
-
-@startuml
-skinparam sequenceArrowThickness 2
-skinparam roundcorner 20
-skinparam maxmessagesize 60
-skinparam sequenceParticipant underline
-
-actor User
-participant "First Class" as A
-participant "Second Class" as B
-participant "Last Class" as C
-
-User -> A: DoWork
-activate A
-
-A -> B: Create Request
-activate B
-
-B -> C: DoWork
-activate C
-C --> B: WorkDone
-destroy C
-
-B --> A: Request Created
-deactivate B
-
-A --> User: Done
-deactivate A
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## クラス図
-
-@startuml
-
-class BaseClass
-
-namespace net.dummy #DDDDDD {
-    .BaseClass <|-- Person
-    Meeting o-- Person
-
-    .BaseClass <|- Meeting
-}
-
-namespace net.foo {
-  net.dummy.Person  <|- Person
-  .BaseClass <|-- Person
-
-  net.dummy.Meeting o-- Person
-}
-
-BaseClass <|-- net.unused.Person
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## コンポーネント図
-
-@startuml
-
-package "Some Group" {
-  HTTP - [First Component]
-  [Another Component]
-}
-
-node "Other Groups" {
-  FTP - [Second Component]
-  [First Component] --> FTP
-}
-
-cloud {
-  [Example 1]
-}
-
-
-database "MySql" {
-  folder "This is my folder" {
-    [Folder 3]
-  }
-  frame "Foo" {
-    [Frame 4]
-  }
-}
-
-
-[Another Component] --> [Example 1]
-[Example 1] --> [Folder 3]
-[Folder 3] --> [Frame 4]
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-
-## ステート図
-
-
-@startuml
-scale 600 width
-
-[*] -> State1
-State1 --> State2 : Succeeded
-State1 --> [*] : Aborted
-State2 --> State3 : Succeeded
-State2 --> [*] : Aborted
-state State3 {
-  state "Accumulate Enough Data\nLong State Name" as long1
-  long1 : Just a test
-  [*] --> long1
-  long1 --> long1 : New Data
-  long1 --> ProcessData : Enough Data
-}
-State3 --> State3 : Failed
-State3 --> [*] : Succeeded / Save Result
-State3 --> [*] : Aborted
-
-@enduml
-
-<!-- Reset PlantUML -->
-<div class="clearfix"></div>
-
-# :pencil: blockdiag
-
-See [blockdiag](http://blockdiag.com/).
-
-## blockdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: blockdiag
-blockdiag {
-   A -> B -> C -> D;
-   A -> E -> F -> G;
-}
-:::
-
-</div>
-
-## seqdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: seqdiag
-seqdiag {
-  browser  -> webserver [label = "GET /index.html"];
-  browser <-- webserver;
-  browser  -> webserver [label = "POST /blog/comment"];
-              webserver  -> database [label = "INSERT comment"];
-              webserver <-- database;
-  browser <-- webserver;
-}
-:::
-
-</div>
-
-## actdiag
 
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
+# :heavy_plus_sign: 更に…
 
-::: actdiag
-actdiag {
-  write -> convert -> image
-
-  lane user {
-     label = "User"
-     write [label = "Writing reST"];
-     image [label = "Get diagram IMAGE"];
-  }
-  lane actdiag {
-     convert [label = "Convert reST to Image"];
-  }
-}
-:::
-
-</div>
-
-## nwdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: nwdiag
-nwdiag {
-  network dmz {
-      address = "210.x.x.x/24"
-
-      web01 [address = "210.x.x.1"];
-      web02 [address = "210.x.x.2"];
-  }
-  network internal {
-      address = "172.x.x.x/24";
-
-      web01 [address = "172.x.x.1"];
-      web02 [address = "172.x.x.2"];
-      db01;
-      db02;
-  }
-}
-:::
-
-</div>
-
-## rackdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: rackdiag
-rackdiag {
-  // define height of rack
-  8U;
-
-  // define rack items
-  1: UPS [2U];
-  3: DB Server
-  4: Web Server
-  5: Web Server
-  6: Web Server
-  7: Load Balancer
-  8: L3 Switch
-}
-:::
-
-</div>
-
-## packetdiag
-
-<!-- Resize blockdiag -->
-<div style="max-width: 600px">
-
-::: packetdiag
-packetdiag {
-  colwidth = 32
-  node_height = 72
-
-  0-15: Source Port
-  16-31: Destination Port
-  32-63: Sequence Number
-  64-95: Acknowledgment Number
-  96-99: Data Offset
-  100-105: Reserved
-  106: URG [rotate = 270]
-  107: ACK [rotate = 270]
-  108: PSH [rotate = 270]
-  109: RST [rotate = 270]
-  110: SYN [rotate = 270]
-  111: FIN [rotate = 270]
-  112-127: Window
-  128-143: Checksum
-  144-159: Urgent Pointer
-  160-191: (Options and Padding)
-  192-223: data [colheight = 3]
-}
-:::
-
-</div>
+- Bootstrap3 のタグを使う
+    - :arrow_right: [/Sandbox/Bootstrap3]
+- 図表を書く
+    - :arrow_right: [/Sandbox/Diagrams]
+- 数式を書く
+    - :arrow_right: [/Sandbox/Math]

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

@@ -388,6 +388,7 @@
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
     "Authentication mechanism settings": "認証機構設定",
+    "setup_is_not_yet_complete":"セットアップはまだ完了してません",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
     "xss_prevent_setting_link": "マークダウン設定ページに移動",
@@ -417,6 +418,7 @@
     "missing mandatory configs": "以下の必須項目の値がデータベースと環境変数のどちらにも設定されていません",
     "Local": {
       "name": "ID/Password",
+      "note for the only env option": "現在LOCAL認証のON/OFFは環境変数の値によって制限されています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "enable_local": "ID/Password を有効にする"
     },
     "ldap": {
@@ -462,8 +464,8 @@
       "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
       "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "attr_based_login_control_detail": "SAMLの <code>&lt;saml:AttributeStatement&gt;</code> 要素に含まれる <code>&lt;saml:Attribute&gt;</code> 要素と、その子要素 <code>&lt;saml:AttributeValue&gt;</code> を利用してログインの可否を制御します。",
-      "attr_based_login_control_rule_detail": "<code>属性名 = 値</code> を <code>|</code>(論理和)、 <code>&</code>(論理積) で連結した形式で記述してください。演算子の優先順位は論理積が論理和より高く、各演算子の結合規則は左から右です。",
-      "attr_based_login_control_rule_example": "例えば <code>Department=A | Department=B & Position=Leader</code> だと <code>Department</code> が <code>A</code> の場合, もしくは<code>Department</code> が <code>B</code> かつ <code>Position</code> が <code>Leader</code> の場合にログインを<strong>許可</strong>します。"
+      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>利用可能なクエリ:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>利用不可なクエリ:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
+      "attr_based_login_control_rule_example": "<h6>Example</h6>ルールに <code>(Department: A || Department: B) && Position: Leader</code> を指定した場合, <code>Department: A</code> または <code>Department: B</code> のどちらかに該当し、かつ <code>Position: Leader</code> を持つユーザーにログインを<strong>許可</strong>します。"
     },
     "Basic": {
       "enable_basic": "Basic を有効にする",

+ 19 - 12
src/client/js/components/Admin/Security/BasicSecuritySetting.jsx

@@ -35,10 +35,11 @@ class BasicSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminBasicSecurityContainer } = this.props;
+    const { t, adminBasicSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminBasicSecurityContainer.updateBasicSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.Basic.updated_basic'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class BasicSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminBasicSecurityContainer } = this.props;
+    const { isBasicEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -56,7 +58,7 @@ class BasicSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          { t('security_setting.Basic.name') } { t('security_setting.configuration') }
+          { t('security_setting.Basic.name') }
         </h2>
 
         {this.state.retrieveError != null && (
@@ -66,7 +68,9 @@ class BasicSecurityManagement extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{ t('security_setting.Basic.name') }</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.Basic.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -85,10 +89,12 @@ class BasicSecurityManagement extends React.Component {
                 { t('security_setting.Basic.desc_2')}
               </small>
             </p>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('basic') && isBasicEnabled)
+            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
-        {adminGeneralSecurityContainer.state.isBasicEnabled && (
+        {isBasicEnabled && (
         <React.Fragment>
           <div className="row mb-5">
             <div className="col-xs-offset-3 col-xs-6 text-left">
@@ -109,16 +115,17 @@ class BasicSecurityManagement extends React.Component {
               </p>
             </div>
           </div>
-        </React.Fragment>
-        )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-4 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              { t('Update') }
-            </button>
+          <div className="row my-3">
+            <div className="col-xs-offset-4 col-xs-5">
+              <button type="button" className="btn btn-primary" disabled={adminBasicSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
+                {t('Update')}
+              </button>
+            </div>
           </div>
-        </div>
+
+        </React.Fragment>
+        )}
 
       </React.Fragment>
     );

+ 20 - 12
src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -35,10 +35,11 @@ class GitHubSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminGitHubSecurityContainer } = this.props;
+    const { t, adminGitHubSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminGitHubSecurityContainer.updateGitHubSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.OAuth.GitHub.updated_github'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class GitHubSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminGitHubSecurityContainer } = this.props;
+    const { isGitHubEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -57,7 +59,7 @@ class GitHubSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.GitHub.name')} {t('security_setting.configuration')}
+          {t('security_setting.OAuth.GitHub.name')}
         </h2>
 
         {this.state.retrieveError != null && (
@@ -67,7 +69,9 @@ class GitHubSecurityManagement extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.OAuth.GitHub.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.GitHub.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -80,6 +84,8 @@ class GitHubSecurityManagement extends React.Component {
                 {t('security_setting.OAuth.GitHub.enable_github')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('github') && isGitHubEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -106,9 +112,11 @@ class GitHubSecurityManagement extends React.Component {
         </div>
 
 
-        {adminGeneralSecurityContainer.state.isGitHubEnabled && (
+        {isGitHubEnabled && (
           <React.Fragment>
 
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <label htmlFor="githubClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
               <div className="col-xs-6">
@@ -161,17 +169,17 @@ class GitHubSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
+                  {t('Update')}
+                </div>
+              </div>
+            </div>
+
           </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <div className="btn btn-primary" disabled={adminGitHubSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </div>
-          </div>
-        </div>
-
         <hr />
 
         <div style={{ minHeight: '300px' }}>

+ 25 - 12
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -35,10 +35,11 @@ class GoogleSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminGoogleSecurityContainer } = this.props;
+    const { t, adminGoogleSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminGoogleSecurityContainer.updateGoogleSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.OAuth.Google.updated_google'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class GoogleSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminGoogleSecurityContainer } = this.props;
+    const { isGoogleEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -57,7 +59,7 @@ class GoogleSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.Google.name')} {t('security_setting.configuration')}
+          {t('security_setting.OAuth.Google.name')}
         </h2>
 
         {this.state.retrieveError != null && (
@@ -67,7 +69,9 @@ class GoogleSecurityManagement extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.OAuth.Google.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.Google.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -80,6 +84,8 @@ class GoogleSecurityManagement extends React.Component {
                 {t('security_setting.OAuth.Google.enable_google')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('google') && isGoogleEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -106,9 +112,11 @@ class GoogleSecurityManagement extends React.Component {
         </div>
 
 
-        {adminGeneralSecurityContainer.state.isGoogleEnabled && (
+        {isGoogleEnabled && (
           <React.Fragment>
 
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <label htmlFor="googleClientId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
               <div className="col-xs-6">
@@ -161,17 +169,22 @@ class GoogleSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminGoogleSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+
           </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminGoogleSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-          </div>
-        </div>
-
         <hr />
 
         <div style={{ minHeight: '300px' }}>

+ 24 - 13
src/client/js/components/Admin/Security/LdapSecuritySetting.jsx

@@ -39,10 +39,11 @@ class LdapSecuritySetting extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminLdapSecurityContainer } = this.props;
+    const { t, adminLdapSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminLdapSecurityContainer.updateLdapSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.ldap.updated_ldap'));
     }
     catch (err) {
@@ -69,11 +70,13 @@ class LdapSecuritySetting extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          LDAP {t('security_setting.configuration')}
+          LDAP
         </h2>
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">Use LDAP</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>Use LDAP</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -86,12 +89,17 @@ class LdapSecuritySetting extends React.Component {
                 {t('security_setting.ldap.enable_ldap')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isLdapEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
 
         {isLdapEnabled && (
           <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <label htmlFor="serverUrl" className="col-xs-3 control-label text-right">Server URL</label>
               <div className="col-xs-6">
@@ -384,20 +392,23 @@ class LdapSecuritySetting extends React.Component {
                 </p>
               </div>
             </div>
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminLdapSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+                <button type="button" className="btn btn-default ml-2" onClick={this.openLdapAuthTestModal}>{t('security_setting.ldap.test_config')}</button>
+              </div>
+            </div>
 
           </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminLdapSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-            {adminGeneralSecurityContainer.state.isLdapEnabled
-              && <button type="button" className="btn btn-default ml-2" onClick={this.openLdapAuthTestModal}>{t('security_setting.ldap.test_config')}</button>
-            }
-          </div>
-        </div>
 
         <LdapAuthTestModal isOpen={this.state.isLdapAuthTestModalShown} onClose={this.closeLdapAuthTestModal} />
 

+ 30 - 14
src/client/js/components/Admin/Security/LocalSecuritySetting.jsx

@@ -35,9 +35,10 @@ class LocalSecuritySetting extends React.Component {
 
 
   async onClickSubmit() {
-    const { t, adminLocalSecurityContainer } = this.props;
+    const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
     try {
       await adminLocalSecurityContainer.updateLocalSecuritySetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.updated_general_security_setting'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class LocalSecuritySetting extends React.Component {
   render() {
     const { t, adminGeneralSecurityContainer, adminLocalSecurityContainer } = this.props;
     const { registrationMode } = adminLocalSecurityContainer.state;
+    const { isLocalEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -61,10 +63,10 @@ class LocalSecuritySetting extends React.Component {
           </div>
         )}
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.Local.name')} {t('security_setting.configuration')}
+          {t('security_setting.Local.name')}
         </h2>
 
-        {adminGeneralSecurityContainer.state.useOnlyEnvVarsForSomeOptions && (
+        {adminLocalSecurityContainer.state.useOnlyEnvVars && (
           <p
             className="alert alert-info"
             // eslint-disable-next-line max-len
@@ -73,7 +75,9 @@ class LocalSecuritySetting extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.Local.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.Local.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -81,16 +85,22 @@ class LocalSecuritySetting extends React.Component {
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isLocalEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsLocalEnabled() }}
+                disabled={adminLocalSecurityContainer.state.useOnlyEnvVars}
               />
               <label htmlFor="isLocalEnabled">
                 {t('security_setting.Local.enable_local')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('local') && isLocalEnabled)
+            && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
-        {adminGeneralSecurityContainer.state.isLocalEnabled && (
-          <div>
+        {isLocalEnabled && (
+          <React.Fragment>
+
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <strong className="col-xs-3 text-right">{t('Register limitation')}</strong>
               <div className="col-xs-9 text-left">
@@ -156,16 +166,22 @@ class LocalSecuritySetting extends React.Component {
                 </div>
               </div>
             </div>
-          </div>
+
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminLocalSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+          </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminLocalSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-          </div>
-        </div>
 
       </React.Fragment>
     );

+ 24 - 11
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -35,10 +35,11 @@ class OidcSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminOidcSecurityContainer } = this.props;
+    const { t, adminOidcSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminOidcSecurityContainer.updateOidcSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.OAuth.OIDC.updated_oidc'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class OidcSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminOidcSecurityContainer } = this.props;
+    const { isOidcEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -58,11 +60,13 @@ class OidcSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.OIDC.name')} {t('security_setting.configuration')}
+          {t('security_setting.OAuth.OIDC.name')}
         </h2>
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.OAuth.OIDC.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.OIDC.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -75,6 +79,8 @@ class OidcSecurityManagement extends React.Component {
                 {t('security_setting.OAuth.enable_oidc')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('oidc') && isOidcEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -100,9 +106,11 @@ class OidcSecurityManagement extends React.Component {
           </div>
         </div>
 
-        {adminGeneralSecurityContainer.state.isOidcEnabled && (
+        {isOidcEnabled && (
           <React.Fragment>
 
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <label htmlFor="oidcProviderName" className="col-xs-3 text-right">{t('security_setting.providerName')}</label>
               <div className="col-xs-6">
@@ -294,16 +302,21 @@ class OidcSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminOidcSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
           </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminOidcSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-          </div>
-        </div>
 
         <hr />
 

+ 26 - 17
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -55,10 +55,11 @@ class SamlSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminSamlSecurityContainer } = this.props;
+    const { t, adminSamlSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminSamlSecurityContainer.updateSamlSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.SAML.updated_saml'));
     }
     catch (err) {
@@ -69,6 +70,7 @@ class SamlSecurityManagement extends React.Component {
   render() {
     const { t, adminGeneralSecurityContainer, adminSamlSecurityContainer } = this.props;
     const { useOnlyEnvVars } = adminSamlSecurityContainer.state;
+    const { isSamlEnabled } = adminGeneralSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -77,7 +79,7 @@ class SamlSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.SAML.name')} {t('security_setting.configuration')}
+          {t('security_setting.SAML.name')}
         </h2>
 
         {useOnlyEnvVars && (
@@ -88,7 +90,9 @@ class SamlSecurityManagement extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.SAML.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.SAML.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -96,11 +100,14 @@ class SamlSecurityManagement extends React.Component {
                 type="checkbox"
                 checked={adminGeneralSecurityContainer.state.isSamlEnabled}
                 onChange={() => { adminGeneralSecurityContainer.switchIsSamlEnabled() }}
+                disabled={adminSamlSecurityContainer.state.useOnlyEnvVars}
               />
               <label htmlFor="isSamlEnabled">
                 {t('security_setting.SAML.enable_saml')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('ldap') && isSamlEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -126,7 +133,7 @@ class SamlSecurityManagement extends React.Component {
           </div>
         </div>
 
-        {adminGeneralSecurityContainer.state.isSamlEnabled && (
+        {isSamlEnabled && (
           <React.Fragment>
 
             {(adminSamlSecurityContainer.state.missingMandatoryConfigKeys.length !== 0) && (
@@ -463,7 +470,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
             </p>
 
-            <table className="table settings-table {% if useOnlyEnvVars %}use-only-env-vars{% endif %}">
+            <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
               <colgroup>
                 <col className="item-name" />
                 <col className="from-db" />
@@ -481,14 +488,13 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                       className="form-control"
                       type="text"
-                      value={adminSamlSecurityContainer.state.samlABLCRule || ''}
+                      defaultValue={adminSamlSecurityContainer.state.samlABLCRule || ''}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       readOnly={useOnlyEnvVars}
                     />
                     <p className="help-block">
                       <small>
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_detail') }} />
-                        <br />
                         <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example') }} />
                       </small>
                     </p>
@@ -508,19 +514,22 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               </tbody>
             </table>
 
-          </React.Fragment>
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminSamlSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
 
+          </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminSamlSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-          </div>
-        </div>
-
-
       </React.Fragment>
     );
 

+ 12 - 14
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -46,11 +46,6 @@ class SecuritySetting extends React.Component {
   render() {
     const { t, adminGeneralSecurityContainer } = this.props;
     const { currentRestrictGuestMode, currentPageCompleteDeletionAuthority } = adminGeneralSecurityContainer.state;
-    const helpPageListingByOwner = { __html: t('security_setting.page_listing_1') };
-    const helpPageListingByGroup = { __html: t('security_setting.page_listing_2') };
-    // eslint-disable-next-line max-len
-    const helpForceWikiMode = { __html: t('security_setting.Fixed by env var', { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }) };
-
 
     return (
       <React.Fragment>
@@ -63,18 +58,17 @@ class SecuritySetting extends React.Component {
               <p>{t('Error occurred')} : {this.state.retrieveError}</p>
             </div>
           )}
-          <div className="row mb-5">
+          <div className="row">
             <strong className="col-xs-3 text-right"> {t('security_setting.Guest Users Access')} </strong>
             <div className="col-xs-9 text-left">
               <div className="my-0 btn-group">
                 <div className="dropdown">
                   <button
-                    className="btn btn-default dropdown-toggle w-100"
+                    className={`btn btn-default dropdown-toggle w-100 ${adminGeneralSecurityContainer.isWikiModeForced && 'disabled'}`}
                     type="button"
                     data-toggle="dropdown"
                     aria-haspopup="true"
                     aria-expanded="false"
-                    disabled={adminGeneralSecurityContainer.state.isWikiModeForced}
                   >
                     <span className="pull-left">
                       {currentRestrictGuestMode === 'Deny' && t('security_setting.guest_mode.deny')}
@@ -107,20 +101,24 @@ class SecuritySetting extends React.Component {
               </div>
             </div>
           </div>
-          {adminGeneralSecurityContainer.state.isWikiModeForced && (
+          {adminGeneralSecurityContainer.isWikiModeForced && (
             <div className="row mb-5">
-              <div className="col-xs-3 text-right" />
-              <div className="col-xs-9 text-left">
+              <div className="col-xs-offset-3 col-xs-6 text-left">
                 <p className="alert alert-warning mt-2 text-left">
                   <i className="icon-exclamation icon-fw">
                   </i><b>FIXED</b><br />
-                  {<b dangerouslySetInnerHTML={helpForceWikiMode} />}
+                  <b
+                    dangerouslySetInnerHTML={{
+                    __html: t('security_setting.Fixed by env var',
+                    { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
+                    }}
+                  />
                 </p>
               </div>
             </div>
           )}
           <div className="row mb-5">
-            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByOwner} />
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_1') }} />
             <div className="col-xs-6 text-left">
               <div className="checkbox checkbox-success">
                 <input
@@ -137,7 +135,7 @@ class SecuritySetting extends React.Component {
           </div>
 
           <div className="row mb-5">
-            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={helpPageListingByGroup} />
+            <strong className="col-xs-3 text-right" dangerouslySetInnerHTML={{ __html: t('security_setting.page_listing_2') }} />
             <div className="col-xs-6 text-left">
               <div className="checkbox checkbox-success">
                 <input

+ 25 - 12
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -35,10 +35,11 @@ class TwitterSecurityManagement extends React.Component {
   }
 
   async onClickSubmit() {
-    const { t, adminTwitterSecurityContainer } = this.props;
+    const { t, adminTwitterSecurityContainer, adminGeneralSecurityContainer } = this.props;
 
     try {
       await adminTwitterSecurityContainer.updateTwitterSetting();
+      await adminGeneralSecurityContainer.retrieveSetupStratedies();
       toastSuccess(t('security_setting.OAuth.Twitter.updated_twitter'));
     }
     catch (err) {
@@ -48,6 +49,7 @@ class TwitterSecurityManagement extends React.Component {
 
   render() {
     const { t, adminGeneralSecurityContainer, adminTwitterSecurityContainer } = this.props;
+    const { isTwitterEnabled } = adminTwitterSecurityContainer.state;
 
     if (this.state.isRetrieving) {
       return null;
@@ -57,7 +59,7 @@ class TwitterSecurityManagement extends React.Component {
       <React.Fragment>
 
         <h2 className="alert-anchor border-bottom">
-          {t('security_setting.OAuth.Twitter.name')} {t('security_setting.configuration')}
+          {t('security_setting.OAuth.Twitter.name')}
         </h2>
 
         {this.state.retrieveError != null && (
@@ -67,7 +69,9 @@ class TwitterSecurityManagement extends React.Component {
         )}
 
         <div className="row mb-5">
-          <strong className="col-xs-3 text-right">{t('security_setting.OAuth.Twitter.name')}</strong>
+          <div className="col-xs-3 my-3 text-right">
+            <strong>{t('security_setting.OAuth.Twitter.name')}</strong>
+          </div>
           <div className="col-xs-6 text-left">
             <div className="checkbox checkbox-success">
               <input
@@ -80,6 +84,8 @@ class TwitterSecurityManagement extends React.Component {
                 {t('security_setting.OAuth.Twitter.enable_twitter')}
               </label>
             </div>
+            {(!adminGeneralSecurityContainer.state.setupStrategies.includes('twitter') && isTwitterEnabled)
+              && <div className="label label-warning">{t('security_setting.setup_is_not_yet_complete')}</div>}
           </div>
         </div>
 
@@ -106,9 +112,11 @@ class TwitterSecurityManagement extends React.Component {
         </div>
 
 
-        {adminGeneralSecurityContainer.state.isTwitterEnabled && (
+        {isTwitterEnabled && (
           <React.Fragment>
 
+            <h3 className="border-bottom">{t('security_setting.configuration')}</h3>
+
             <div className="row mb-5">
               <label htmlFor="TwitterConsumerId" className="col-xs-3 text-right">{t('security_setting.clientID')}</label>
               <div className="col-xs-6">
@@ -161,17 +169,22 @@ class TwitterSecurityManagement extends React.Component {
               </div>
             </div>
 
+            <div className="row my-3">
+              <div className="col-xs-offset-3 col-xs-5">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  disabled={adminTwitterSecurityContainer.state.retrieveError != null}
+                  onClick={this.onClickSubmit}
+                >
+                  {t('Update')}
+                </button>
+              </div>
+            </div>
+
           </React.Fragment>
         )}
 
-        <div className="row my-3">
-          <div className="col-xs-offset-3 col-xs-5">
-            <button type="button" className="btn btn-primary" disabled={adminTwitterSecurityContainer.state.retrieveError != null} onClick={this.onClickSubmit}>
-              {t('Update')}
-            </button>
-          </div>
-        </div>
-
         <hr />
 
         <div style={{ minHeight: '300px' }}>

+ 2 - 1
src/client/js/components/Drawio.jsx

@@ -24,7 +24,7 @@ class Drawio extends React.Component {
 
   onEdit() {
     if (window.crowi != null) {
-      window.crowi.launchDrawioIFrame('page',
+      window.crowi.launchDrawioModal('page',
         this.props.rangeLineNumberOfMarkdown.beginLineNumber,
         this.props.rangeLineNumberOfMarkdown.endLineNumber);
     }
@@ -58,6 +58,7 @@ class Drawio extends React.Component {
           onScroll={(event) => {
             event.preventDefault();
           }}
+          // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{ __html: this.renderContents() }}
         >
         </div>

+ 7 - 7
src/client/js/components/Page.jsx

@@ -11,7 +11,7 @@ import MarkdownTable from '../models/MarkdownTable';
 
 import RevisionRenderer from './Page/RevisionRenderer';
 import HandsontableModal from './PageEditor/HandsontableModal';
-import DrawioIFrame from './PageEditor/DrawioIFrame';
+import DrawioModal from './PageEditor/DrawioModal';
 import mtu from './PageEditor/MarkdownTableUtil';
 import mdu from './PageEditor/MarkdownDrawioUtil';
 
@@ -30,7 +30,7 @@ class Page extends React.Component {
     this.growiRenderer = this.props.appContainer.getRenderer('page');
 
     this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
-    this.saveHandlerForDrawioIFrame = this.saveHandlerForDrawioIFrame.bind(this);
+    this.saveHandlerForDrawioModal = this.saveHandlerForDrawioModal.bind(this);
   }
 
   componentWillMount() {
@@ -50,16 +50,16 @@ class Page extends React.Component {
   }
 
   /**
-   * launch DrawioIFrame with data specified by arguments
+   * launch DrawioModal with data specified by arguments
    * @param beginLineNumber
    * @param endLineNumber
    */
-  launchDrawioIFrame(beginLineNumber, endLineNumber) {
+  launchDrawioModal(beginLineNumber, endLineNumber) {
     const markdown = this.props.pageContainer.state.markdown;
     const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber, endLineNumber);
     const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
     this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
-    this.drawioIFrame.show(drawioData);
+    this.drawioModal.show(drawioData);
   }
 
   async saveHandlerForHandsontableModal(markdownTable) {
@@ -92,7 +92,7 @@ class Page extends React.Component {
     }
   }
 
-  async saveHandlerForDrawioIFrame(drawioData) {
+  async saveHandlerForDrawioModal(drawioData) {
     const { pageContainer, editorContainer } = this.props;
     const optionsToSave = editorContainer.getCurrentOptionsToSave();
 
@@ -130,7 +130,7 @@ class Page extends React.Component {
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
         <HandsontableModal ref={(c) => { this.handsontableModal = c }} onSave={this.saveHandlerForHandsontableModal} />
-        <DrawioIFrame ref={(c) => { this.drawioIFrame = c }} onSave={this.saveHandlerForDrawioIFrame} />
+        <DrawioModal ref={(c) => { this.drawioModal = c }} onSave={this.saveHandlerForDrawioModal} />
       </div>
     );
   }

+ 2 - 2
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -20,7 +20,7 @@ import mtu from './MarkdownTableUtil';
 import mdu from './MarkdownDrawioUtil';
 import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
-import DrawioIFrame from './DrawioIFrame';
+import DrawioModal from './DrawioModal';
 
 const loadScript = require('simple-load-script');
 const loadCssSync = require('load-css-file');
@@ -839,7 +839,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
           ref={(c) => { this.handsontableModal = c }}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
         />
-        <DrawioIFrame
+        <DrawioModal
           ref={(c) => { this.drawioIFrame = c }}
           onSave={(drawioData) => { return mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioData) }}
         />

+ 29 - 45
src/client/js/components/PageEditor/DrawioIFrame.jsx → src/client/js/components/PageEditor/DrawioModal.jsx

@@ -2,20 +2,16 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import i18next from 'i18next';
 
-export default class DrawioIFrame extends React.PureComponent {
+import Modal from 'react-bootstrap/es/Modal';
+
+export default class DrawioModal extends React.PureComponent {
 
   constructor(props) {
     super(props);
 
     this.state = {
-      shown: false,
+      show: false,
       drawioMxFile: '',
-      style: {
-        zIndex: 9999,
-        top: 0,
-        left: 0,
-        bottom: 0,
-      },
     };
 
     this.drawioIFrame = React.createRef();
@@ -26,7 +22,6 @@ export default class DrawioIFrame extends React.PureComponent {
     this.init = this.init.bind(this);
     this.cancel = this.cancel.bind(this);
     this.receiveFromDrawio = this.receiveFromDrawio.bind(this);
-    this.onResizeWindow = this.onResizeWindow.bind(this);
     this.drawioURL = this.drawioURL.bind(this);
   }
 
@@ -42,21 +37,13 @@ export default class DrawioIFrame extends React.PureComponent {
   show(drawioMxFile) {
     this.init(drawioMxFile);
 
-    this.setState({
-      style: Object.assign({}, this.state.style, {
-        width: window.innerWidth,
-        height: window.innerHeight,
-      }),
-    });
-
-    window.addEventListener('resize', this.onResizeWindow);
     window.addEventListener('message', this.receiveFromDrawio);
-    this.setState({ shown: true });
+    this.setState({ show: true });
   }
 
   hide() {
     this.setState({
-      shown: false,
+      show: false,
     });
   }
 
@@ -106,7 +93,6 @@ export default class DrawioIFrame extends React.PureComponent {
         this.props.onSave(value);
       }
 
-      window.removeEventListener('resize', this.onResizeWindow);
       window.removeEventListener('message', this.receiveFromDrawio);
       this.hide();
 
@@ -114,7 +100,6 @@ export default class DrawioIFrame extends React.PureComponent {
     }
 
     if (typeof event.data === 'string' && event.data.length === 0) {
-      window.removeEventListener('resize', this.onResizeWindow);
       window.removeEventListener('message', this.receiveFromDrawio);
       this.hide();
 
@@ -124,15 +109,6 @@ export default class DrawioIFrame extends React.PureComponent {
     // NOTHING DONE. (Receive unknown iframe message.)
   }
 
-  onResizeWindow(event) {
-    this.setState({
-      style: Object.assign({}, this.state.style, {
-        width: window.innerWidth,
-        height: window.innerHeight,
-      }),
-    });
-  }
-
   drawioURL() {
     const url = new URL('https://www.draw.io/');
 
@@ -148,25 +124,33 @@ export default class DrawioIFrame extends React.PureComponent {
 
   render() {
     return (
-      <div>
-        {
-          this.state.shown
-          && (
-            <iframe
-              ref={(c) => { this.drawioIFrame = c }}
-              src={this.drawioURL()}
-              className="position-fixed"
-              style={this.state.style}
-            >
-            </iframe>
-          )
-        }
-      </div>
+      // <Modal show={this.state.show} onHide={this.cancel} bsSize="large" dialogClassName={dialogClassName} keyboard={false}>
+      <Modal show={this.state.show} onHide={this.cancel} dialogClassName="drawio-modal" bsSize="large" keyboard={false}>
+        <Modal.Body className="p-0">
+          {/* Loading spinner */}
+          <div className="w-100 h-100 position-absolute d-flex">
+            <div className="mx-auto my-auto">
+              <i className="fa fa-3x fa-spinner fa-pulse mx-auto text-muted"></i>
+            </div>
+          </div>
+          {/* iframe */}
+          <div className="w-100 h-100 position-absolute d-flex">
+            { this.state.show && (
+              <iframe
+                ref={(c) => { this.drawioIFrame = c }}
+                src={this.drawioURL()}
+                className="border-0 flex-grow-1"
+              >
+              </iframe>
+            ) }
+          </div>
+        </Modal.Body>
+      </Modal>
     );
   }
 
 }
 
-DrawioIFrame.propTypes = {
+DrawioModal.propTypes = {
   onSave: PropTypes.func,
 };

+ 25 - 14
src/client/js/services/AdminGeneralSecurityContainer.js

@@ -15,13 +15,11 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.appContainer = appContainer;
 
     this.state = {
-      isWikiModeForced: false,
       wikiMode: '',
       currentRestrictGuestMode: 'Deny',
       currentPageCompleteDeletionAuthority: 'adminOnly',
       isShowRestrictedByOwner: false,
       isShowRestrictedByGroup: false,
-      useOnlyEnvVarsForSomeOptions: false,
       appSiteUrl: appContainer.config.crowi.url || '',
       isLocalEnabled: false,
       isLdapEnabled: false,
@@ -31,15 +29,15 @@ export default class AdminGeneralSecurityContainer extends Container {
       isGoogleEnabled: false,
       isGitHubEnabled: false,
       isTwitterEnabled: false,
+      setupStrategies: [],
     };
 
-    this.onIsWikiModeForced = this.onIsWikiModeForced.bind(this);
   }
 
   async retrieveSecurityData() {
+    await this.retrieveSetupStratedies();
     const response = await this.appContainer.apiv3.get('/security-setting/');
     const { generalSetting, generalAuth } = response.data.securityParams;
-    this.onIsWikiModeForced(generalSetting.wikiMode);
     this.setState({
       currentPageCompleteDeletionAuthority: generalSetting.pageCompleteDeletionAuthority,
       isShowRestrictedByOwner: !generalSetting.hideRestrictedByOwner,
@@ -64,6 +62,14 @@ export default class AdminGeneralSecurityContainer extends Container {
     return 'AdminGeneralSecurityContainer';
   }
 
+  /**
+   * get isWikiModeForced
+   * @return {bool} isWikiModeForced
+   */
+  get isWikiModeForced() {
+    return this.state.wikiMode === 'public' || this.state.wikiMode === 'private';
+  }
+
   /**
    * Change restrictGuestMode
    */
@@ -92,16 +98,6 @@ export default class AdminGeneralSecurityContainer extends Container {
     this.setState({ isShowRestrictedByGroup:  !this.state.isShowRestrictedByGroup });
   }
 
-  onIsWikiModeForced(wikiModeSetting) {
-    if (wikiModeSetting === 'private') {
-      this.setState({ isWikiModeForced: true });
-    }
-    else {
-      this.setState({ isWikiModeForced: false });
-    }
-  }
-
-
   /**
    * Update restrictGuestMode
    * @memberOf AdminGeneralSecuritySContainer
@@ -132,6 +128,7 @@ export default class AdminGeneralSecurityContainer extends Container {
         isEnabled,
         authId,
       });
+      await this.retrieveSetupStratedies();
       this.setState({ [stateVariableName]: isEnabled });
     }
     catch (err) {
@@ -139,6 +136,20 @@ export default class AdminGeneralSecurityContainer extends Container {
     }
   }
 
+  /**
+   * Retrieve SetupStratedies
+   */
+  async retrieveSetupStratedies() {
+    try {
+      const response = await this.appContainer.apiv3.get('/security-setting/authentication');
+      const { setupStrategies } = response.data;
+      this.setState({ setupStrategies });
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
   /**
    * Switch local enabled
    */

+ 3 - 2
src/client/js/services/AdminLocalSecurityContainer.js

@@ -18,6 +18,7 @@ export default class AdminLocalSecurityContainer extends Container {
       retrieveError: null,
       registrationMode: 'Open',
       registrationWhiteList: [],
+      useOnlyEnvVars: false,
     };
 
   }
@@ -27,6 +28,7 @@ export default class AdminLocalSecurityContainer extends Container {
       const response = await this.appContainer.apiv3.get('/security-setting/');
       const { localSetting } = response.data.securityParams;
       this.setState({
+        useOnlyEnvVars: localSetting.useOnlyEnvVarsForSomeOptions,
         registrationMode: localSetting.registrationMode,
         registrationWhiteList: localSetting.registrationWhiteList,
       });
@@ -65,8 +67,7 @@ export default class AdminLocalSecurityContainer extends Container {
    * update local security setting
    */
   async updateLocalSecuritySetting() {
-    let { registrationWhiteList } = this.state;
-    registrationWhiteList = Array.isArray(registrationWhiteList) ? registrationWhiteList : registrationWhiteList.split('\n');
+    const { registrationWhiteList } = this.state;
     const response = await this.appContainer.apiv3.put('/security-setting/local-setting', {
       registrationMode: this.state.registrationMode,
       registrationWhiteList,

+ 2 - 0
src/client/js/services/AdminSamlSecurityContainer.js

@@ -21,6 +21,7 @@ export default class AdminSamlSecurityContainer extends Container {
 
     this.state = {
       retrieveError: null,
+      // TODO GW-1324 ABLCRure DB value takes precedence
       useOnlyEnvVars: false,
       callbackUrl: urljoin(pathUtils.removeTrailingSlash(appContainer.config.crowi.url), '/passport/saml/callback'),
       missingMandatoryConfigKeys: [],
@@ -48,6 +49,7 @@ export default class AdminSamlSecurityContainer extends Container {
       const { samlAuth } = response.data.securityParams;
       this.setState({
         missingMandatoryConfigKeys: samlAuth.missingMandatoryConfigKeys,
+        useOnlyEnvVars: samlAuth.useOnlyEnvVarsForSomeOptions,
         samlEntryPoint: samlAuth.samlEntryPoint,
         samlIssuer: samlAuth.samlIssuer,
         samlCert: samlAuth.samlCert,

+ 2 - 2
src/client/js/services/AppContainer.js

@@ -304,14 +304,14 @@ export default class AppContainer extends Container {
     targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
   }
 
-  launchDrawioIFrame(componentKind, beginLineNumber, endLineNumber) {
+  launchDrawioModal(componentKind, beginLineNumber, endLineNumber) {
     let targetComponent;
     switch (componentKind) {
       case 'page':
         targetComponent = this.getComponentInstance('Page');
         break;
     }
-    targetComponent.launchDrawioIFrame(beginLineNumber, endLineNumber);
+    targetComponent.launchDrawioModal(beginLineNumber, endLineNumber);
   }
 
   async apiGet(path, params) {

+ 3 - 0
src/client/styles/scss/_drawio.scss

@@ -0,0 +1,3 @@
+.drawio-modal {
+  @include expand-modal-fullscreen(false, false);
+}

+ 4 - 17
src/client/styles/scss/_handsontable.scss

@@ -10,24 +10,11 @@
 
 // expanded window layout
 .handsontable-modal.handsontable-modal-expanded {
-  // full-screen modal
-  width: 97%;
-  height: 95%;
-  .modal-content {
-    height: 95%;
-  }
-
-  // expand .modal-body (with calculating height)
-  .modal-body {
-    $modal-header: 54px;
-    $modal-footer: 46px;
-    $margin: $modal-header + $modal-footer;
-    height: calc(100% - #{$margin});
+  @include expand-modal-fullscreen(true, true);
 
-    // expand .hot-table-container (with flexbox)
-    .hot-table-container {
-      flex: 1;
-    }
+  // expand .hot-table-container (with flexbox)
+  .hot-table-container {
+    flex: 1;
   }
 }
 

+ 26 - 0
src/client/styles/scss/_mixins.scss

@@ -83,3 +83,29 @@
     }
   }
 }
+
+@mixin expand-modal-fullscreen($hasModalHeader: true, $hasModalFooter: true) {
+  // full-screen modal
+  width: auto;
+  height: calc(100vh - 30px);
+  margin: 15px;
+
+  .modal-content {
+    height: calc(100vh - 30px);
+  }
+
+  // expand .modal-body (with calculating height)
+  .modal-body {
+    $modal-header: 54px;
+    $modal-footer: 46px;
+
+    $margin: 0px;
+    @if $hasModalHeader {
+      $margin: $margin + $modal-header;
+    }
+    @if $hasModalFooter {
+      $margin: $margin + $modal-footer;
+    }
+    height: calc(100% - #{$margin});
+  }
+}

+ 1 - 0
src/client/styles/scss/style-app.scss

@@ -18,6 +18,7 @@
 @import 'comment_crowi';
 @import 'comment_growi';
 @import 'comment_kibela';
+@import 'drawio';
 @import 'navbar_kibela';
 @import 'create-page';
 @import 'create-template';

+ 3 - 0
src/lib/service/logger/stream.prod.js

@@ -17,6 +17,9 @@ else {
     const bunyanFormat = require('bunyan-format');
     stream = bunyanFormat({ outputMode: 'long' });
   }
+  else {
+    stream = process.stdout;
+  }
 }
 
 module.exports = stream;

+ 2 - 2
src/server/crowi/express-init.js

@@ -19,7 +19,7 @@ module.exports = function(crowi, app) {
   const i18nSprintf = require('i18next-sprintf-postprocessor');
   const i18nMiddleware = require('i18next-express-middleware');
 
-  const safeRedirect = require('../middleware/safe-redirect')();
+  const registerSafeRedirect = require('../middleware/safe-redirect')();
 
   const avoidSessionRoutes = require('../routes/avoid-session-routes');
   const i18nUserSettingDetector = require('../util/i18nUserSettingDetector');
@@ -115,7 +115,7 @@ module.exports = function(crowi, app) {
 
   app.use(flash());
 
-  app.use(safeRedirect);
+  app.use(registerSafeRedirect);
 
   const middlewares = require('../util/middlewares')(crowi, app);
 

+ 45 - 9
src/server/routes/apiv3/security-setting.js

@@ -1,6 +1,3 @@
-
-/* eslint-disable max-len */
-/* eslint-disable no-unused-vars */
 const loggerFactory = require('@alias/logger');
 
 const logger = loggerFactory('growi:routes:apiv3:security-setting');
@@ -34,7 +31,9 @@ const validator = {
     body('registrationMode').isString().isIn([
       'Open', 'Restricted', 'Closed',
     ]),
-    body('registrationWhiteList').if((value, { req }) => req.body.registrationWhiteList).isArray(),
+    body('registrationWhiteList').if((value, { req }) => req.body.registrationWhiteList).isArray().customSanitizer((value, { req }) => {
+      return value.filter(email => email !== '');
+    }),
   ],
   ldapAuth: [
     body('serverUrl').if((value, { req }) => req.body.serverUrl).isString(),
@@ -323,6 +322,7 @@ module.exports = (crowi) => {
         wikiMode: await crowi.configManager.getConfig('crowi', 'security:wikiMode'),
       },
       localSetting: {
+        useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfig('crowi', 'security:passport-local:useOnlyEnvVarsForSomeOptions'),
         registrationMode: await crowi.configManager.getConfig('crowi', 'security:registrationMode'),
         registrationWhiteList: await crowi.configManager.getConfig('crowi', 'security:registrationWhiteList'),
       },
@@ -352,6 +352,7 @@ module.exports = (crowi) => {
       },
       samlAuth: {
         missingMandatoryConfigKeys: await crowi.passportService.getSamlMissingMandatoryConfigKeys(),
+        useOnlyEnvVarsForSomeOptions: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:useOnlyEnvVarsForSomeOptions'),
         samlEntryPoint: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:entryPoint'),
         samlEnvVarEntryPoint: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:entryPoint'),
         samlIssuer: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:issuer'),
@@ -370,7 +371,7 @@ module.exports = (crowi) => {
         samlEnvVarAttrMapLastName: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:attrMapLastName'),
         isSameUsernameTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameUsernameTreatedAsIdenticalUser'),
         isSameEmailTreatedAsIdenticalUser: await crowi.configManager.getConfig('crowi', 'security:passport-saml:isSameEmailTreatedAsIdenticalUser'),
-        samlABLCRule: await crowi.configManager.getConfig('crowi', 'security:passport-saml:ABLCRule'),
+        samlABLCRule: await crowi.configManager.getConfigFromDB('crowi', 'security:passport-saml:ABLCRule'),
         samlEnvVarABLCRule: await crowi.configManager.getConfigFromEnvVars('crowi', 'security:passport-saml:ABLCRule'),
       },
       oidcAuth: {
@@ -453,7 +454,9 @@ module.exports = (crowi) => {
 
       await crowi.passportService.setupStrategyById(authId);
 
-      const responseParams = { [`security:passport-${authId}:isEnabled`]: await crowi.configManager.getConfig('crowi', `security:passport-${authId}:isEnabled`) };
+      const responseParams = {
+        [`security:passport-${authId}:isEnabled`]: await crowi.configManager.getConfig('crowi', `security:passport-${authId}:isEnabled`),
+      };
 
       return res.apiv3({ responseParams });
     }
@@ -466,6 +469,34 @@ module.exports = (crowi) => {
 
   });
 
+  /**
+   * @swagger
+   *
+   *    /_api/v3/security-setting/authentication:
+   *      get:
+   *        tags: [SecuritySetting, apiv3]
+   *        description: Get setup strategies for passport
+   *        responses:
+   *          200:
+   *            description: params of setup strategies
+   *            content:
+   *              application/json:
+   *                schema:
+   *                  properties:
+   *                    setupStrategies:
+   *                      type: array
+   *                      description: setup strategies list
+   *                      items:
+   *                        type: string
+   *                        description: setup strategie
+   *                      example: ["local"]
+   */
+  router.get('/authentication/', loginRequiredStrictly, adminRequired, async(req, res) => {
+    const setupStrategies = await crowi.passportService.getSetupStrategies();
+
+    return res.apiv3({ setupStrategies });
+  });
+
   /**
    * @swagger
    *
@@ -495,7 +526,7 @@ module.exports = (crowi) => {
       'security:list-policy:hideRestrictedByGroup': req.body.hideRestrictedByGroup,
     };
     const wikiMode = await crowi.configManager.getConfig('crowi', 'security:wikiMode');
-    if (wikiMode === 'private') {
+    if (wikiMode === 'private' || wikiMode === 'public') {
       logger.debug('security:restrictGuestMode will not be changed because wiki mode is forced to set');
       delete requestParams['security:restrictGuestMode'];
     }
@@ -663,8 +694,13 @@ module.exports = (crowi) => {
     const rule = req.body.ABLCRule;
     // Empty string disables attribute-based login control.
     // So, when rule is empty string, validation is passed.
-    if (rule != null && (rule == null || crowi.passportService.parseABLCRule(rule) == null)) {
-      return res.apiv3Err(req.t('form_validation.invalid_syntax', req.t('security_setting.form_item_name.ABLCRule')), 400);
+    if (rule != null) {
+      try {
+        crowi.passportService.parseABLCRule(rule);
+      }
+      catch (err) {
+        return res.apiv3Err(req.t('form_validation.invalid_syntax', req.t('security_setting.form_item_name.ABLCRule')), 400);
+      }
     }
 
     const requestParams = {

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

@@ -20,23 +20,22 @@ module.exports = function(crowi, app) {
     await searchService.rebuildIndex();
   }
 
+  async function createPage(filePath, pagePath, owner, lang) {
+    const markdown = fs.readFileSync(filePath);
+    return Page.create(pagePath, markdown, owner, {});
+  }
+
   async function createInitialPages(owner, lang) {
     const promises = [];
 
     // create portal page for '/'
-    const welcomeMarkdownPath = path.join(crowi.localeDir, lang, 'welcome.md');
-    const welcomeMarkdown = fs.readFileSync(welcomeMarkdownPath);
-    promises.push(Page.create('/', welcomeMarkdown, owner, {}));
-
-    // create /Sandbox
-    const sandboxMarkdownPath = path.join(crowi.localeDir, lang, 'sandbox.md');
-    const sandboxMarkdown = fs.readFileSync(sandboxMarkdownPath);
-    promises.push(Page.create('/Sandbox', sandboxMarkdown, owner, {}));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'welcome.md'), '/', owner, lang));
 
-    // create /Sandbox/Bootstrap3
-    const bs3MarkdownPath = path.join(crowi.localeDir, 'en-US', 'sandbox-bootstrap3.md');
-    const bs3Markdown = fs.readFileSync(bs3MarkdownPath);
-    promises.push(Page.create('/Sandbox/Bootstrap3', bs3Markdown, owner, {}));
+    // create /Sandbox/*
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox.md'), '/Sandbox', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-bootstrap3.md'), '/Sandbox/Bootstrap3', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner, lang));
+    promises.push(createPage(path.join(crowi.localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner, lang));
 
     await Promise.all(promises);
 
@@ -68,6 +67,7 @@ module.exports = function(crowi, app) {
     await appService.initDB(language);
 
     // create first admin user
+    // TODO: with transaction
     let adminUser;
     try {
       adminUser = await User.createUser(name, username, email, password, language);

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

@@ -56,7 +56,8 @@ module.exports = function(crowi, app) {
 
   actions.preLogin = function(req, res, next) {
     // user has already logged in
-    if (req.user != null) {
+    const { user } = req;
+    if (user != null && user.status === User.STATUS_ACTIVE) {
       const { redirectTo } = req.session;
       // remove session.redirectTo
       delete req.session.redirectTo;

+ 14 - 6
src/server/service/passport.js

@@ -666,21 +666,29 @@ class PassportService {
     return missingRequireds;
   }
 
+  /**
+   * Parse Attribute-Based Login Control Rule as Lucene Query
+   * @param {string} rule Lucene syntax string
+   * @returns {object} Expression Tree Structure generated by lucene-query-parser
+   * @see https://github.com/thoward/lucene-query-parser.js/wiki
+   */
+  parseABLCRule(rule) {
+    // parse with lucene-query-parser
+    // see https://github.com/thoward/lucene-query-parser.js/wiki
+    return luceneQueryParser.parse(rule);
+  }
+
   /**
    * Verify that a SAML response meets the attribute-base login control rule
    */
   verifySAMLResponseByABLCRule(response) {
     const rule = this.crowi.configManager.getConfig('crowi', 'security:passport-saml:ABLCRule');
     if (rule == null) {
+      debug('There is no ABLCRule.');
       return true;
     }
 
-    // parse with lucene-query-parser
-    // see https://github.com/thoward/lucene-query-parser.js/wiki
-    const luceneRule = luceneQueryParser.parse(rule);
-    if (luceneRule == null) {
-      return false;
-    }
+    const luceneRule = this.parseABLCRule(rule);
     debug({ 'Parsed Rule': JSON.stringify(luceneRule, null, 2) });
 
     const attributes = this.extractAttributesFromSAMLResponse(response);

+ 8 - 8
src/test/middleware/safe-redirect.test.js

@@ -1,7 +1,7 @@
 /* eslint-disable arrow-body-style */
 
 describe('safeRedirect', () => {
-  let safeRedirect;
+  let registerSafeRedirect;
 
   const whitelistOfHosts = [
     'white1.example.com:8080',
@@ -9,7 +9,7 @@ describe('safeRedirect', () => {
   ];
 
   beforeEach(async(done) => {
-    safeRedirect = require('@server/middleware/safe-redirect')(whitelistOfHosts);
+    registerSafeRedirect = require('@server/middleware/safe-redirect')(whitelistOfHosts);
     done();
   });
 
@@ -26,7 +26,7 @@ describe('safeRedirect', () => {
     const next = jest.fn();
 
     test('redirects to \'/\' because specified url causes open redirect vulnerability', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('//evil.example.com');
 
@@ -39,7 +39,7 @@ describe('safeRedirect', () => {
     });
 
     test('redirects to \'/\' because specified host without port is not in whitelist', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('http://white1.example.com/path/to/page');
 
@@ -52,7 +52,7 @@ describe('safeRedirect', () => {
     });
 
     test('redirects to the specified local url', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('/path/to/page');
 
@@ -65,7 +65,7 @@ describe('safeRedirect', () => {
     });
 
     test('redirects to the specified local url (fqdn)', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('http://example.com/path/to/page');
 
@@ -78,7 +78,7 @@ describe('safeRedirect', () => {
     });
 
     test('redirects to the specified whitelisted url (white1.example.com:8080)', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('http://white1.example.com:8080/path/to/page');
 
@@ -91,7 +91,7 @@ describe('safeRedirect', () => {
     });
 
     test('redirects to the specified whitelisted url (white2.example.com:8080)', () => {
-      safeRedirect(req, res, next);
+      registerSafeRedirect(req, res, next);
 
       const result = res.safeRedirect('http://white2.example.com:8080/path/to/page');
 

+ 23 - 15
yarn.lock

@@ -1277,6 +1277,11 @@
     "@types/yargs" "^15.0.0"
     chalk "^3.0.0"
 
+"@kaishuu0123/markdown-it-fence@0.1.4", "@kaishuu0123/markdown-it-fence@^0.1.4":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@kaishuu0123/markdown-it-fence/-/markdown-it-fence-0.1.4.tgz#759cf0dd80cca23a08e70b9cbb33c999cb23f3c3"
+  integrity sha512-u00GhVLpTeIbeflMKCozzaCAEmuwGngryomtbsYoyRwdYLfrH2nJHZa41+gwKLXsrq7Ii3N+Rei5GHaqHT3j5A==
+
 "@lykmapipo/common@>=0.21.0":
   version "0.21.0"
   resolved "https://registry.yarnpkg.com/@lykmapipo/common/-/common-0.21.0.tgz#fe06e54e3c02479a8c303fb7843341fee83f60ab"
@@ -8333,33 +8338,29 @@ markdown-escapes@^1.0.0:
   resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.3.tgz#6155e10416efaafab665d466ce598216375195f5"
   integrity sha512-XUi5HJhhV5R74k8/0H2oCbCiYf/u4cO/rX8tnGkRvrqhsr5BRNU6Mg0yt/8UIx1iIS8220BNJsDb7XnILhLepw==
 
-markdown-it-blockdiag@^1.0.2:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.0.2.tgz#30f055a0d5e3dc7195a9055ebbd404381261fe8b"
+markdown-it-blockdiag@^1.0.3:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/markdown-it-blockdiag/-/markdown-it-blockdiag-1.0.3.tgz#7c2be967d21a17f559da5860b6179b34f29192a3"
+  integrity sha512-2y4C5L6V3twWhDdQLl3zzYvBiumKcS7D1CD8/OTN2Y57G1gE2ot86vTPrfljaf2Q8q6J1UgcG40zTjDGcHgHrg==
   dependencies:
-    markdown-it-fence "0.1.3"
+    "@kaishuu0123/markdown-it-fence" "0.1.4"
     pako "^1.0.6"
     paths "^0.1.1"
     url-join "^4.0.0"
     utf8-bytes "0.0.1"
 
-markdown-it-drawio-viewer@^1.1.0:
-  version "1.1.2"
-  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.2.tgz#4ae442545d32bd44bb9f8ca18085315cea1ec692"
-  integrity sha512-YD+wZ4SDipZf1+TeHcUDWhGqN+FaiA+77be1flcfCli1a4cGy3fo8r2QnPRAn+rVEhTpMB6cQsUS5I0KxwHFYQ==
+markdown-it-drawio-viewer@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.3.tgz#e5f7fa9a1d200a8711e2aeb691551add413ffe33"
+  integrity sha512-FELyH+ko9sOAC6hHQqBYi4tI1Qv8HTzVPXsYuwTFy0NF5gY1A9oriN051rHn4UtyAAXEUIMquNNYYkxC9bhMVw==
   dependencies:
-    markdown-it-fence "0.1.3"
+    "@kaishuu0123/markdown-it-fence" "^0.1.4"
     xmldoc "^1.1.2"
 
 markdown-it-emoji@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
 
-markdown-it-fence@0.1.3:
-  version "0.1.3"
-  resolved "https://registry.yarnpkg.com/markdown-it-fence/-/markdown-it-fence-0.1.3.tgz#26e90149b7de5658cdb27096b2e2b5ec4e72d6ce"
-  integrity sha512-mdZbkwJUyZXhyDX4QzD+WnIhxWBq9oIIJU22E6jCT7MHgIp79oEOJfwXIl/HqmfIxnXEdC47sBGn4h6chM4DKw==
-
 markdown-it-footnote@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/markdown-it-footnote/-/markdown-it-footnote-3.0.1.tgz#7f3730747cacc86e2fe0bf8a17a710f34791517a"
@@ -9909,7 +9910,12 @@ pako@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.3.tgz#5f515b0c6722e1982920ae8005eacb0b7ca73ccf"
 
-pako@^1.0.6, pako@~1.0.5:
+pako@^1.0.6:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+pako@~1.0.5:
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258"
 
@@ -10177,6 +10183,7 @@ path-type@^4.0.0:
 paths@^0.1.1:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/paths/-/paths-0.1.1.tgz#9ad909d7f769dd8acb3a1c033c5eef43123d3d17"
+  integrity sha1-mtkJ1/dp3YrLOhwDPF7vQxI9PRc=
 
 pause-stream@0.0.11:
   version "0.0.11"
@@ -13890,6 +13897,7 @@ uslug@^1.0.4:
 utf8-bytes@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/utf8-bytes/-/utf8-bytes-0.0.1.tgz#116b025448c9b500081cdfbf1f4d6c6c37d8837d"
+  integrity sha1-EWsCVEjJtQAIHN+/H01sbDfYg30=
 
 util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"

Некоторые файлы не были показаны из-за большого количества измененных файлов