Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,29 @@ public abstract class AbstractSwaggerUiConfigProperties {
*/
protected Boolean withCredentials;

/**
* The Document title.
*/
protected String documentTitle;

/**
* Gets document title.
*
* @return the document title
*/
public String getDocumentTitle() {
return documentTitle;
}

/**
* Sets document title.
*
* @param documentTitle the document title
*/
public void setDocumentTitle(String documentTitle) {
this.documentTitle = documentTitle;
}

/**
* Gets with credentials.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public class AbstractSwaggerIndexTransformer {
*/
private static final String PRESETS = "presets: [";

/**
* The constant EDITOR_FOLD_MARKER.
*/
private static final String EDITOR_FOLD_MARKER = "//</editor-fold>";

/**
* The Swagger ui o auth properties.
*/
Expand Down Expand Up @@ -178,6 +183,9 @@ else if (swaggerUiConfig.getCsrf().isUseSessionStorage())
html = setConfiguredApiDocsUrl(html);
}

if (StringUtils.isNotEmpty(swaggerUiConfig.getDocumentTitle()))
html = addDocumentTitle(html);

return html;
}

Expand Down Expand Up @@ -325,4 +333,41 @@ protected String addSyntaxHighlight(String html) {
return html.replace(PRESETS, stringBuilder.toString());
}

/**
* Add document title script.
*
* @param html the html
* @return the string
*/
protected String addDocumentTitle(String html) {
if (!html.contains(EDITOR_FOLD_MARKER)) {
return html;
}
StringBuilder stringBuilder = new StringBuilder("document.title = '");
stringBuilder.append(escapeJavaScriptString(swaggerUiConfig.getDocumentTitle()));
stringBuilder.append("';\n\n ");
stringBuilder.append(EDITOR_FOLD_MARKER);
return html.replace(EDITOR_FOLD_MARKER, stringBuilder.toString());
}

/**
* Escape special characters for JavaScript string literal.
*
* @param input the input string
* @return the escaped string
*/
private String escapeJavaScriptString(String input) {
if (input == null) {
return "";
}
return input
.replace("\\", "\\\\")
.replace("'", "\\'")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
.replace("<", "\\u003c")
.replace(">", "\\u003e");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;

@ExtendWith(MockitoExtension.class)
class AbstractSwaggerIndexTransformerTest {
Expand Down Expand Up @@ -67,4 +68,63 @@ void setApiDocUrlCorrectly() throws IOException {
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), is);
assertThat(html, containsString(apiDocUrl));
}

@Test
void documentTitle_whenSet_addsDocumentTitleScript() throws IOException {
swaggerUiConfig.setDocumentTitle("My Custom API Documentation");
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, containsString("document.title = 'My Custom API Documentation';"));
}

@Test
void documentTitle_whenNotSet_doesNotAddScript() throws IOException {
swaggerUiConfig.setDocumentTitle(null);
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, not(containsString("document.title")));
}

@Test
void documentTitle_whenEmpty_doesNotAddScript() throws IOException {
swaggerUiConfig.setDocumentTitle("");
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, not(containsString("document.title")));
}

@Test
void documentTitle_escapesSpecialCharacters() throws IOException {
swaggerUiConfig.setDocumentTitle("Test's API \\ Documentation");
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, containsString("document.title = 'Test\\'s API \\\\ Documentation';"));
}

@Test
void documentTitle_escapesNewlines() throws IOException {
swaggerUiConfig.setDocumentTitle("Test\nAPI\rDocs\tTitle");
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, containsString("document.title = 'Test\\nAPI\\rDocs\\tTitle';"));
}

@Test
void documentTitle_escapesScriptTags() throws IOException {
swaggerUiConfig.setDocumentTitle("</script><script>alert('xss')</script>");
InputStream inputStream = new ByteArrayInputStream(swaggerInitJs.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, not(containsString("</script><script>")));
assertThat(html, containsString("\\u003c/script\\u003e\\u003cscript\\u003ealert"));
}

@Test
void documentTitle_whenMarkerMissing_returnsOriginalHtml() throws IOException {
String htmlWithoutMarker = "window.onload = function() { window.ui = SwaggerUIBundle({}); };";
swaggerUiConfig.setDocumentTitle("My Title");
swaggerUiConfig.setUrl(null);
InputStream inputStream = new ByteArrayInputStream(htmlWithoutMarker.getBytes(StandardCharsets.UTF_8));
var html = underTest.defaultTransformations(new SwaggerUiConfigParameters(swaggerUiConfig), inputStream);
assertThat(html, not(containsString("document.title")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
*
* * Copyright 2019-2020 the original author or authors.
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * https://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/

package test.org.springdoc.ui.app39;

import org.junit.jupiter.api.Test;
import org.springdoc.core.utils.Constants;
import test.org.springdoc.ui.AbstractSpringDocTest;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.test.context.TestPropertySource;

import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@TestPropertySource(properties = {
"springdoc.swagger-ui.document-title=My Custom API Documentation"
})
public class SpringDocApp39Test extends AbstractSpringDocTest {

@Test
void transformedIndexWithDocumentTitle() throws Exception {
mockMvc.perform(get(Constants.SWAGGER_INITIALIZER_URL))
.andExpect(status().isOk())
.andExpect(content().string(containsString("document.title = 'My Custom API Documentation';")));
}

@SpringBootApplication
static class SpringDocTestApp {}
}