Skip to content

Commit 11e96ec

Browse files
committed
feat(swagger-ui): add springdoc.swagger-ui.document-title property
#3208
1 parent bb8ae1f commit 11e96ec

4 files changed

Lines changed: 177 additions & 2 deletions

File tree

springdoc-openapi-starter-common/src/main/java/org/springdoc/core/properties/AbstractSwaggerUiConfigProperties.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,29 @@ public abstract class AbstractSwaggerUiConfigProperties {
185185
*/
186186
protected Boolean withCredentials;
187187

188+
/**
189+
* The Document title.
190+
*/
191+
protected String documentTitle;
192+
193+
/**
194+
* Gets document title.
195+
*
196+
* @return the document title
197+
*/
198+
public String getDocumentTitle() {
199+
return documentTitle;
200+
}
201+
202+
/**
203+
* Sets document title.
204+
*
205+
* @param documentTitle the document title
206+
*/
207+
public void setDocumentTitle(String documentTitle) {
208+
this.documentTitle = documentTitle;
209+
}
210+
188211
/**
189212
* Gets with credentials.
190213
*

springdoc-openapi-starter-common/src/main/java/org/springdoc/ui/AbstractSwaggerIndexTransformer.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ public class AbstractSwaggerIndexTransformer {
5959
*/
6060
private static final String PRESETS = "presets: [";
6161

62+
/**
63+
* The constant EDITOR_FOLD_MARKER.
64+
*/
65+
private static final String EDITOR_FOLD_MARKER = "//</editor-fold>";
66+
6267
/**
6368
* The Swagger ui o auth properties.
6469
*/
@@ -178,6 +183,9 @@ else if (swaggerUiConfig.getCsrf().isUseSessionStorage())
178183
html = setConfiguredApiDocsUrl(html);
179184
}
180185

186+
if (StringUtils.isNotEmpty(swaggerUiConfig.getDocumentTitle()))
187+
html = addDocumentTitle(html);
188+
181189
return html;
182190
}
183191

@@ -325,4 +333,41 @@ protected String addSyntaxHighlight(String html) {
325333
return html.replace(PRESETS, stringBuilder.toString());
326334
}
327335

328-
}
336+
/**
337+
* Add document title script.
338+
*
339+
* @param html the html
340+
* @return the string
341+
*/
342+
protected String addDocumentTitle(String html) {
343+
if (!html.contains(EDITOR_FOLD_MARKER)) {
344+
return html;
345+
}
346+
StringBuilder stringBuilder = new StringBuilder("document.title = '");
347+
stringBuilder.append(escapeJavaScriptString(swaggerUiConfig.getDocumentTitle()));
348+
stringBuilder.append("';\n\n ");
349+
stringBuilder.append(EDITOR_FOLD_MARKER);
350+
return html.replace(EDITOR_FOLD_MARKER, stringBuilder.toString());
351+
}
352+
353+
/**
354+
* Escape special characters for JavaScript string literal.
355+
*
356+
* @param input the input string
357+
* @return the escaped string
358+
*/
359+
private String escapeJavaScriptString(String input) {
360+
if (input == null) {
361+
return "";
362+
}
363+
return input
364+
.replace("\\", "\\\\")
365+
.replace("'", "\\'")
366+
.replace("\n", "\\n")
367+
.replace("\r", "\\r")
368+
.replace("\t", "\\t")
369+
.replace("<", "\\u003c")
370+
.replace(">", "\\u003e");
371+
}
372+
373+
}

springdoc-openapi-starter-common/src/test/java/org/springdoc/ui/AbstractSwaggerIndexTransformerTest.java

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.hamcrest.MatcherAssert.assertThat;
2020
import static org.hamcrest.Matchers.containsString;
21+
import static org.hamcrest.Matchers.not;
2122

2223
@ExtendWith(MockitoExtension.class)
2324
class AbstractSwaggerIndexTransformerTest {
@@ -67,4 +68,63 @@ void setApiDocUrlCorrectly() throws IOException {
6768
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), is);
6869
assertThat(html, containsString(apiDocUrl));
6970
}
70-
}
71+
72+
@Test
73+
void documentTitle_whenSet_addsDocumentTitleScript() throws IOException {
74+
swaggerUiConfig.setDocumentTitle("My Custom API Documentation");
75+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
76+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
77+
assertThat(html, containsString("document.title = 'My Custom API Documentation';"));
78+
}
79+
80+
@Test
81+
void documentTitle_whenNotSet_doesNotAddScript() throws IOException {
82+
swaggerUiConfig.setDocumentTitle(null);
83+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
84+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
85+
assertThat(html, not(containsString("document.title")));
86+
}
87+
88+
@Test
89+
void documentTitle_whenEmpty_doesNotAddScript() throws IOException {
90+
swaggerUiConfig.setDocumentTitle("");
91+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
92+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
93+
assertThat(html, not(containsString("document.title")));
94+
}
95+
96+
@Test
97+
void documentTitle_escapesSpecialCharacters() throws IOException {
98+
swaggerUiConfig.setDocumentTitle("Test's API \\ Documentation");
99+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
100+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
101+
assertThat(html, containsString("document.title = 'Test\\'s API \\\\ Documentation';"));
102+
}
103+
104+
@Test
105+
void documentTitle_escapesNewlines() throws IOException {
106+
swaggerUiConfig.setDocumentTitle("Test\nAPI\rDocs\tTitle");
107+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
108+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
109+
assertThat(html, containsString("document.title = 'Test\\nAPI\\rDocs\\tTitle';"));
110+
}
111+
112+
@Test
113+
void documentTitle_escapesScriptTags() throws IOException {
114+
swaggerUiConfig.setDocumentTitle("</script><script>alert('xss')</script>");
115+
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
116+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
117+
assertThat(html, not(containsString("</script><script>")));
118+
assertThat(html, containsString("\\u003c/script\\u003e\\u003cscript\\u003ealert"));
119+
}
120+
121+
@Test
122+
void documentTitle_whenMarkerMissing_returnsOriginalHtml() throws IOException {
123+
String htmlWithoutMarker = "window.onload = function() { window.ui = SwaggerUIBundle({}); };";
124+
swaggerUiConfig.setDocumentTitle("My Title");
125+
swaggerUiConfig.setUrl(null);
126+
InputStream inputStream = new ByteArrayInputStream(htmlWithoutMarker.getBytes(StandardCharsets.UTF_8));
127+
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
128+
assertThat(html, not(containsString("document.title")));
129+
}
130+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
*
3+
* * Copyright 2019-2020 the original author or authors.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * https://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package test.org.springdoc.ui.app39;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.springdoc.core.utils.Constants;
23+
import test.org.springdoc.ui.AbstractSpringDocTest;
24+
25+
import org.springframework.boot.autoconfigure.SpringBootApplication;
26+
import org.springframework.test.context.TestPropertySource;
27+
28+
import static org.hamcrest.Matchers.containsString;
29+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
30+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
31+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
32+
33+
@TestPropertySource(properties = {
34+
"springdoc.swagger-ui.document-title=My Custom API Documentation"
35+
})
36+
public class SpringDocApp39Test extends AbstractSpringDocTest {
37+
38+
@Test
39+
void transformedIndexWithDocumentTitle() throws Exception {
40+
mockMvc.perform(get(Constants.SWAGGER_INITIALIZER_URL))
41+
.andExpect(status().isOk())
42+
.andExpect(content().string(containsString("document.title = 'My Custom API Documentation';")));
43+
}
44+
45+
@SpringBootApplication
46+
static class SpringDocTestApp {}
47+
}

0 commit comments

Comments
 (0)