From 558a2afbd27ec350325a49aa703a73989492bae3 Mon Sep 17 00:00:00 2001 From: Wyatt Allen Date: Wed, 15 Mar 2017 09:55:31 -0700 Subject: [PATCH] Serve PolyGerrit index.html from a Soy template Rather than serving a static file, serve the PolyGerrit index HTML document from a Soy template. In this way, the path to load PG dependencies can be safely parameterized in two ways: * If Gerrit is not running on the root of the domain (e.g. listening on https://example.com/my-gerrit/) the path component of the Canonical Web URL is used to load PG dependencies. Instead of /elements/gr-app.js off the root of the domain it uses /my-gerrit/elements/gr-app.js * If the PolyGerrit static resources are to be served from a CDN rather than the Gerrit WAR, the `cdnPath` config in the [gerrit] section can be used. For example, if the server config says ... [gerrit] cdnPath = http://my-cdn.com/pg/version/123 ... then it uses the following style of path for PG dependencies. http://my-cdn.com/pg/version/123/my-gerrit/elements/gr-app.js If a CDN-path is configured, it supersedes subdirectories appearing in the Canonical-Web-URL for this purpose. Feature: Issue 5845 Change-Id: I2b2d704fe33c90ea2f2a2183fc79897642a48175 (cherry picked from commit 414659c792ab369afc5de4793855bd185dcef14e) --- Documentation/config-gerrit.txt | 4 + gerrit-httpd/BUILD | 3 + .../google/gerrit/httpd/raw/IndexServlet.java | 85 +++++++++++++++++++ .../google/gerrit/httpd/raw/StaticModule.java | 10 ++- .../google/gerrit/httpd/raw/index.html.soy | 42 +++++++++ .../gerrit/httpd/raw/IndexServletTest.java | 56 ++++++++++++ polygerrit-ui/app/elements/gr-app.js | 15 +++- polygerrit-ui/app/elements/gr-app_test.html | 9 +- 8 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java create mode 100644 gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy create mode 100644 gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt index 2c34b2eefb..a6db7421f4 100644 --- a/Documentation/config-gerrit.txt +++ b/Documentation/config-gerrit.txt @@ -2059,6 +2059,10 @@ Setting this option to true will prevent this behavior. + By default false. +[[gerrit.cdnPath]]gerrit.cdnPath:: ++ +Path prefix for PolyGerrit's static resources if using a CDN. + [[gitweb]] === Section gitweb diff --git a/gerrit-httpd/BUILD b/gerrit-httpd/BUILD index 6244b5bd34..9b1b610e03 100644 --- a/gerrit-httpd/BUILD +++ b/gerrit-httpd/BUILD @@ -37,6 +37,7 @@ java_library( "//lib:jsch", "//lib:mime-util", "//lib:servlet-api-3_1", + "//lib:soy", "//lib/auto:auto-value", "//lib/commons:codec", "//lib/guice", @@ -54,6 +55,7 @@ junit_tests( srcs = glob(["src/test/java/**/*.java"]), deps = [ ":httpd", + "//gerrit-common:annotations", "//gerrit-common:server", "//gerrit-extension-api:api", "//gerrit-reviewdb:server", @@ -66,6 +68,7 @@ junit_tests( "//lib:jimfs", "//lib:junit", "//lib:servlet-api-3_1-without-neverlink", + "//lib:soy", "//lib:truth", "//lib/easymock", "//lib/guice", diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java new file mode 100644 index 0000000000..d7f7a8a9ce --- /dev/null +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/IndexServlet.java @@ -0,0 +1,85 @@ +// Copyright (C) 2017 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 java.nio.charset.StandardCharsets.UTF_8; +import static javax.servlet.http.HttpServletResponse.SC_OK; + +import com.google.common.io.Resources; +import com.google.gerrit.common.Nullable; +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.tofu.SoyTofu; +import com.google.template.soy.data.UnsafeSanitizedContentOrdainer; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public class IndexServlet extends HttpServlet { + private final byte[] indexSource; + + IndexServlet(String canonicalURL, @Nullable String cdnPath) throws URISyntaxException { + String resourcePath = "com/google/gerrit/httpd/raw/index.html.soy"; + SoyFileSet.Builder builder = SoyFileSet.builder(); + builder.add(Resources.getResource(resourcePath)); + SoyTofu.Renderer renderer = builder.build().compileToTofu() + .newRenderer("com.google.gerrit.httpd.raw.Index") + .setContentKind(SanitizedContent.ContentKind.HTML) + .setData(getTemplateData(canonicalURL, cdnPath)); + indexSource = renderer.render().getBytes(); + } + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException { + rsp.setCharacterEncoding(UTF_8.name()); + rsp.setContentType("text/html"); + rsp.setStatus(SC_OK); + try (OutputStream w = rsp.getOutputStream()) { + w.write(indexSource); + } + } + + static String computeCanonicalPath(String canonicalURL) throws URISyntaxException { + // 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 getTemplateData(String canonicalURL, String cdnPath) 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); + } +} diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java index 350417ddfc..6e055a9b96 100644 --- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java +++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticModule.java @@ -27,11 +27,13 @@ import com.google.gerrit.httpd.XsrfCookieFilter; import com.google.gerrit.httpd.raw.ResourceServlet.Resource; import com.google.gerrit.launcher.GerritLauncher; import com.google.gerrit.server.cache.CacheModule; +import com.google.gerrit.server.config.CanonicalWebUrl; import com.google.gerrit.server.config.GerritOptions; import com.google.gerrit.server.config.GerritServerConfig; import com.google.gerrit.server.config.SitePaths; import com.google.inject.Inject; import com.google.inject.Key; +import com.google.inject.Provider; import com.google.inject.Provides; import com.google.inject.ProvisionException; import com.google.inject.Singleton; @@ -41,6 +43,7 @@ import com.google.inject.servlet.ServletModule; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.nio.file.Path; import javax.servlet.Filter; @@ -249,9 +252,10 @@ public class StaticModule extends ServletModule { @Provides @Singleton @Named(POLYGERRIT_INDEX_SERVLET) - HttpServlet getPolyGerritUiIndexServlet(@Named(CACHE) Cache cache) { - return new SingleFileServlet( - cache, polyGerritBasePath().resolve("index.html"), getPaths().isDev(), false); + HttpServlet getPolyGerritUiIndexServlet(@CanonicalWebUrl @Nullable String canonicalUrl, + @GerritServerConfig Config cfg) throws URISyntaxException { + String cdnPath = cfg.getString("gerrit", null, "cdnPath"); + return new IndexServlet(canonicalUrl, cdnPath); } @Provides diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy new file mode 100644 index 0000000000..7b828b6f7c --- /dev/null +++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/index.html.soy @@ -0,0 +1,42 @@ +/** + * Copyright (C) 2017 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. + */ + +{namespace com.google.gerrit.httpd.raw} + +/** + * @param canonicalPath + * @param staticResourcePath + */ +{template .Index autoescape="strict" kind="html"} + {\n} + {\n} + {\n} + {\n} + {\n} + + // SourceCodePro fonts are used in styles/fonts.css + // @see https://github.com/w3c/preload/issues/32 regarding crossorigin + {\n} + {\n} + {\n} + {\n} + {\n} + {\n} + {\n} + + {\n} + {\n} +{/template} diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java new file mode 100644 index 0000000000..ffdbc0b568 --- /dev/null +++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/raw/IndexServletTest.java @@ -0,0 +1,56 @@ +// Copyright (C) 2017 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 com.google.gerrit.common.Nullable; +import com.google.template.soy.data.SoyMapData; +import java.net.URISyntaxException; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; + +public class IndexServletTest { + @Test + public void noPathAndNoCDN() throws URISyntaxException { + SoyMapData data = IndexServlet.getTemplateData("http://example.com/", null); + assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo(""); + assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo(""); + } + + @Test + public void pathAndNoCDN() throws URISyntaxException { + SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit/", null); + assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit"); + assertThat(data.getSingle("staticResourcePath").stringValue()).isEqualTo("/gerrit"); + } + + @Test + public void noPathAndCDN() throws URISyntaxException { + SoyMapData data = IndexServlet.getTemplateData("http://example.com/", + "http://my-cdn.com/foo/bar/"); + assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo(""); + assertThat(data.getSingle("staticResourcePath").stringValue()) + .isEqualTo("http://my-cdn.com/foo/bar/"); + } + + @Test + public void pathAndCDN() throws URISyntaxException { + SoyMapData data = IndexServlet.getTemplateData("http://example.com/gerrit", + "http://my-cdn.com/foo/bar/"); + assertThat(data.getSingle("canonicalPath").stringValue()).isEqualTo("/gerrit"); + assertThat(data.getSingle("staticResourcePath").stringValue()) + .isEqualTo("http://my-cdn.com/foo/bar/"); + } +} diff --git a/polygerrit-ui/app/elements/gr-app.js b/polygerrit-ui/app/elements/gr-app.js index 2e60f77dd8..8b3592c506 100644 --- a/polygerrit-ui/app/elements/gr-app.js +++ b/polygerrit-ui/app/elements/gr-app.js @@ -37,6 +37,12 @@ value: function() { return document.body; }, }, + /** + * The path component of the canonicalWebURL. If Gerrit is running from + * the root of the domain, this should be empty. + */ + canonicalPath: String, + _account: { type: Object, observer: '_accountChanged', @@ -73,8 +79,11 @@ '?': '_showKeyboardShortcuts', }, - attached: function() { + ready: function() { + Gerrit.CANONICAL_PATH = this.canonicalPath; + this.$.router.start(); + this.$.restAPI.getAccount().then(function(account) { this._account = account; }.bind(this)); @@ -84,9 +93,7 @@ this.$.restAPI.getVersion().then(function(version) { this._version = version; }.bind(this)); - }, - ready: function() { this.$.reporting.appStarted(); this._viewState = { changeView: { @@ -108,6 +115,8 @@ }, _accountChanged: function(account) { + if (!account) { return; } + // Preferences are cached when a user is logged in; warm them. this.$.restAPI.getPreferences(); this.$.restAPI.getDiffPreferences(); diff --git a/polygerrit-ui/app/elements/gr-app_test.html b/polygerrit-ui/app/elements/gr-app_test.html index d03ab794fd..9c709b02da 100644 --- a/polygerrit-ui/app/elements/gr-app_test.html +++ b/polygerrit-ui/app/elements/gr-app_test.html @@ -25,7 +25,7 @@ limitations under the License. @@ -39,6 +39,9 @@ limitations under the License. stub('gr-reporting', { appStarted: sandbox.stub(), }); + stub('gr-account-dropdown', { + _getTopContent: sinon.stub(), + }); stub('gr-rest-api-interface', { getAccount: function() { return Promise.resolve(null); }, getConfig: function() { @@ -94,5 +97,9 @@ limitations under the License. element._loadPlugins([]); assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0)); }); + + test('canonical-path', function() { + assert.equal(Gerrit.CANONICAL_PATH, '/abc/def/ghi'); + }); });