Factor out template generation code into reusable class

This refactoring improves readability and lets us use the dynamic
content part inside Google.

For now, we have to pass in a converter to sanitize a string to be used
in a script tag because our internal version of Soy does not support
UnsafeSanitizedContentOrdainer.

Remove SoyDataMap because it is marked as deprecated.

Change-Id: I6d34708c98c6a4edae75defb6f45a0eab4c976bf
This commit is contained in:
Patrick Hiesel
2019-05-29 14:22:13 +02:00
parent d06b463ad7
commit 15766d77cd
4 changed files with 211 additions and 113 deletions

View File

@@ -0,0 +1,135 @@
// Copyright (C) 2019 The Android Open Source Project
//
// 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
//
// http://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 com.google.gerrit.httpd.raw;
import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.UsedAt;
import com.google.gerrit.common.UsedAt.Project;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
import com.google.gson.Gson;
import com.google.template.soy.data.SanitizedContent;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/** Helper for generating parts of {@code index.html}. */
public class IndexHtmlUtil {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/**
* Returns both static and dynamic parameters of {@code index.html}. The result is to be used when
* rendering the soy template.
*/
public static ImmutableMap<String, Object> templateData(
GerritApi gerritApi,
String canonicalURL,
String cdnPath,
String faviconPath,
Function<String, SanitizedContent> urlInScriptTagOrdainer)
throws URISyntaxException, RestApiException {
return ImmutableMap.<String, Object>builder()
.putAll(staticTemplateData(canonicalURL, cdnPath, faviconPath, urlInScriptTagOrdainer))
.putAll(dynamicTemplateData(gerritApi))
.build();
}
/** Returns dynamic parameters of {@code index.html}. */
@UsedAt(Project.GOOGLE)
public static Map<String, Map<String, SanitizedContent>> dynamicTemplateData(GerritApi gerritApi)
throws RestApiException {
Gson gson = OutputFormat.JSON_COMPACT.newGson();
Map<String, SanitizedContent> initialData = new HashMap<>();
Server serverApi = gerritApi.config().server();
initialData.put("\"/config/server/info\"", serializeObject(gson, serverApi.getInfo()));
initialData.put("\"/config/server/version\"", serializeObject(gson, serverApi.getVersion()));
initialData.put("\"/config/server/top-menus\"", serializeObject(gson, serverApi.topMenus()));
try {
AccountApi accountApi = gerritApi.accounts().self();
initialData.put("\"/accounts/self/detail\"", serializeObject(gson, accountApi.get()));
initialData.put(
"\"/accounts/self/preferences\"", serializeObject(gson, accountApi.getPreferences()));
initialData.put(
"\"/accounts/self/preferences.diff\"",
serializeObject(gson, accountApi.getDiffPreferences()));
initialData.put(
"\"/accounts/self/preferences.edit\"",
serializeObject(gson, accountApi.getEditPreferences()));
} catch (AuthException e) {
logger.atFine().withCause(e).log(
"Can't inline account-related data because user is unauthenticated");
// Don't render data
// TODO(hiesel): Tell the client that the user is not authenticated so that it doesn't have to
// fetch anyway. This requires more client side modifications.
}
return ImmutableMap.of("gerritInitialData", initialData);
}
/** Returns all static parameters of {@code index.html}. */
static Map<String, Object> staticTemplateData(
String canonicalURL,
String cdnPath,
String faviconPath,
Function<String, SanitizedContent> urlInScriptTagOrdainer)
throws URISyntaxException {
String canonicalPath = computeCanonicalPath(canonicalURL);
String staticPath = "";
if (cdnPath != null) {
staticPath = cdnPath;
} else if (canonicalPath != null) {
staticPath = canonicalPath;
}
SanitizedContent sanitizedStaticPath = urlInScriptTagOrdainer.apply(staticPath);
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
if (canonicalPath != null) {
data.put("canonicalPath", canonicalPath);
}
if (sanitizedStaticPath != null) {
data.put("staticResourcePath", sanitizedStaticPath);
}
if (faviconPath != null) {
data.put("faviconPath", faviconPath);
}
return data.build();
}
private static String computeCanonicalPath(@Nullable String canonicalURL)
throws URISyntaxException {
if (Strings.isNullOrEmpty(canonicalURL)) {
return "";
}
// If we serving from a sub-directory rather than root, determine the path
// from the cannonical web URL.
URI uri = new URI(canonicalURL);
return uri.getPath().replaceAll("/$", "");
}
private IndexHtmlUtil() {}
}

View File

@@ -14,38 +14,26 @@
package com.google.gerrit.httpd.raw;
import static com.google.template.soy.data.ordainers.GsonOrdainer.serializeObject;
import static java.nio.charset.StandardCharsets.UTF_8;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import com.google.common.base.Strings;
import com.google.common.flogger.FluentLogger;
import com.google.common.io.Resources;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.accounts.AccountApi;
import com.google.gerrit.extensions.api.config.Server;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
import com.google.gson.Gson;
import com.google.template.soy.SoyFileSet;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
import com.google.template.soy.tofu.SoyTofu;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class IndexServlet extends HttpServlet {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final long serialVersionUID = 1L;
@Nullable private final String canonicalUrl;
@@ -53,6 +41,7 @@ public class IndexServlet extends HttpServlet {
@Nullable private final String faviconPath;
private final GerritApi gerritApi;
private final SoyTofu soyTofu;
private final Function<String, SanitizedContent> urlOrdainer;
IndexServlet(
@Nullable String canonicalUrl,
@@ -68,19 +57,24 @@ public class IndexServlet extends HttpServlet {
.add(Resources.getResource("com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy"))
.build()
.compileToTofu();
this.urlOrdainer =
(s) ->
UnsafeSanitizedContentOrdainer.ordainAsSafe(
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
SoyTofu.Renderer renderer;
try {
SoyMapData templateData = getStaticTemplateData(canonicalUrl, cdnPath, faviconPath);
templateData.put("gerritInitialData", getInitialData());
// TODO(hiesel): Remove URL ordainer as parameter once Soy is consistent
renderer =
soyTofu
.newRenderer("com.google.gerrit.httpd.raw.Index")
.setContentKind(SanitizedContent.ContentKind.HTML)
.setData(templateData);
.setData(
IndexHtmlUtil.templateData(
gerritApi, canonicalUrl, cdnPath, faviconPath, urlOrdainer));
} catch (URISyntaxException | RestApiException e) {
throw new IOException(e);
}
@@ -92,67 +86,4 @@ public class IndexServlet extends HttpServlet {
w.write(renderer.render().getBytes(UTF_8));
}
}
static String computeCanonicalPath(@Nullable String canonicalURL) throws URISyntaxException {
if (Strings.isNullOrEmpty(canonicalURL)) {
return "";
}
// If we serving from a sub-directory rather than root, determine the path
// from the cannonical web URL.
URI uri = new URI(canonicalURL);
return uri.getPath().replaceAll("/$", "");
}
static SoyMapData getStaticTemplateData(String canonicalURL, String cdnPath, String faviconPath)
throws URISyntaxException {
String canonicalPath = computeCanonicalPath(canonicalURL);
String staticPath = "";
if (cdnPath != null) {
staticPath = cdnPath;
} else if (canonicalPath != null) {
staticPath = canonicalPath;
}
// The resource path must be typed as safe for use in a script src.
// TODO(wyatta): Upgrade this to use an appropriate safe URL type.
SanitizedContent sanitizedStaticPath =
UnsafeSanitizedContentOrdainer.ordainAsSafe(
staticPath, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
return new SoyMapData(
"canonicalPath", canonicalPath,
"staticResourcePath", sanitizedStaticPath,
"faviconPath", faviconPath);
}
private Map<String, SanitizedContent> getInitialData() throws RestApiException {
Gson gson = OutputFormat.JSON_COMPACT.newGson();
Map<String, SanitizedContent> initialData = new HashMap<>();
Server serverApi = gerritApi.config().server();
initialData.put("\"/config/server/info\"", serializeObject(gson, serverApi.getInfo()));
initialData.put("\"/config/server/version\"", serializeObject(gson, serverApi.getVersion()));
initialData.put("\"/config/server/top-menus\"", serializeObject(gson, serverApi.topMenus()));
try {
AccountApi accountApi = gerritApi.accounts().self();
initialData.put("\"/accounts/self/detail\"", serializeObject(gson, accountApi.get()));
initialData.put(
"\"/accounts/self/preferences\"", serializeObject(gson, accountApi.getPreferences()));
initialData.put(
"\"/accounts/self/preferences.diff\"",
serializeObject(gson, accountApi.getDiffPreferences()));
initialData.put(
"\"/accounts/self/preferences.edit\"",
serializeObject(gson, accountApi.getEditPreferences()));
} catch (AuthException e) {
logger.atFine().withCause(e).log(
"Can't inline account-related data because user is unauthenticated");
// Don't render data
// TODO(hiesel): Tell the client that the user is not authenticated so that it doesn't have to
// fetch anyway. This requires more client side modifications.
}
return initialData;
}
}

View File

@@ -0,0 +1,66 @@
// Copyright (C) 2019 The Android Open Source Project
//
// 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
//
// http://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 com.google.gerrit.httpd.raw;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.httpd.raw.IndexHtmlUtil.staticTemplateData;
import com.google.template.soy.data.SanitizedContent;
import com.google.template.soy.data.UnsafeSanitizedContentOrdainer;
import org.junit.Test;
public class IndexHtmlUtilTest {
@Test
public void noPathAndNoCDN() throws Exception {
assertThat(staticTemplateData("http://example.com/", null, null, IndexHtmlUtilTest::ordain))
.containsExactly("canonicalPath", "", "staticResourcePath", ordain(""));
}
@Test
public void pathAndNoCDN() throws Exception {
assertThat(
staticTemplateData("http://example.com/gerrit/", null, null, IndexHtmlUtilTest::ordain))
.containsExactly("canonicalPath", "/gerrit", "staticResourcePath", ordain("/gerrit"));
}
@Test
public void noPathAndCDN() throws Exception {
assertThat(
staticTemplateData(
"http://example.com/",
"http://my-cdn.com/foo/bar/",
null,
IndexHtmlUtilTest::ordain))
.containsExactly(
"canonicalPath", "", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
}
@Test
public void pathAndCDN() throws Exception {
assertThat(
staticTemplateData(
"http://example.com/gerrit",
"http://my-cdn.com/foo/bar/",
null,
IndexHtmlUtilTest::ordain))
.containsExactly(
"canonicalPath", "/gerrit", "staticResourcePath", ordain("http://my-cdn.com/foo/bar/"));
}
private static SanitizedContent ordain(String s) {
return UnsafeSanitizedContentOrdainer.ordainAsSafe(
s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
}
}

View File

@@ -29,43 +29,9 @@ import com.google.gerrit.extensions.common.ServerInfo;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.util.http.testutil.FakeHttpServletRequest;
import com.google.gerrit.util.http.testutil.FakeHttpServletResponse;
import com.google.template.soy.data.SoyMapData;
import org.junit.Test;
public class IndexServletTest {
@Test
public void noPathAndNoCDN() throws Exception {
SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("");
}
@Test
public void pathAndNoCDN() throws Exception {
SoyMapData data = IndexServlet.getStaticTemplateData("http://example.com/gerrit/", null, null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit");
}
@Test
public void noPathAndCDN() throws Exception {
SoyMapData data =
IndexServlet.getStaticTemplateData(
"http://example.com/", "http://my-cdn.com/foo/bar/", null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("");
assertThat(data.getSingle("staticResourcePath").stringValue())
.isEqualTo("http://my-cdn.com/foo/bar/");
}
@Test
public void pathAndCDN() throws Exception {
SoyMapData data =
IndexServlet.getStaticTemplateData(
"http://example.com/gerrit", "http://my-cdn.com/foo/bar/", null);
assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit");
assertThat(data.getSingle("staticResourcePath").stringValue())
.isEqualTo("http://my-cdn.com/foo/bar/");
}
@Test
public void renderTemplate() throws Exception {