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
This commit is contained in:
Wyatt Allen 2017-03-15 09:55:31 -07:00
parent baf902fae0
commit 414659c792
8 changed files with 217 additions and 7 deletions

View File

@ -2065,6 +2065,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

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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<Path, Resource> 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

View File

@ -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"}
<!DOCTYPE html>{\n}
<html lang="en">{\n}
<meta charset="utf-8">{\n}
<meta name="description" content="Gerrit Code Review">{\n}
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
// SourceCodePro fonts are used in styles/fonts.css
// @see https://github.com/w3c/preload/issues/32 regarding crossorigin
<link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff2" as="font" type="font/woff2" crossorigin>{\n}
<link rel="preload" href="{$staticResourcePath}/fonts/SourceCodePro-Regular.woff" as="font" type="font/woff" crossorigin>{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/fonts.css">{\n}
<link rel="stylesheet" href="{$staticResourcePath}/styles/main.css">{\n}
<script src="{$staticResourcePath}/bower_components/webcomponentsjs/webcomponents-lite.js"></script>{\n}
<link rel="preload" href="{$staticResourcePath}/elements/gr-app.js" as="script" crossorigin="anonymous">{\n}
<link rel="import" href="{$staticResourcePath}/elements/gr-app.html">{\n}
<body unresolved>{\n}
<gr-app id="app" canonical-path="{$canonicalPath}"></gr-app>{\n}
{/template}

View File

@ -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/");
}
}

View File

@ -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',
@ -72,8 +78,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));
@ -83,9 +92,7 @@
this.$.restAPI.getVersion().then(function(version) {
this._version = version;
}.bind(this));
},
ready: function() {
this.$.reporting.appStarted();
this._viewState = {
changeView: {
@ -107,6 +114,8 @@
},
_accountChanged: function(account) {
if (!account) { return; }
// Preferences are cached when a user is logged in; warm them.
this.$.restAPI.getPreferences();
this.$.restAPI.getDiffPreferences();

View File

@ -27,7 +27,7 @@ limitations under the License.
<test-fixture id="basic">
<template>
<gr-app id="app"></gr-app>
<gr-app id="app" canonical-path="/abc/def/ghi"></gr-app>
</template>
</test-fixture>
@ -41,6 +41,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({}); },
getAccountCapabilities: function() { return Promise.resolve({}); },
@ -99,5 +102,9 @@ limitations under the License.
done();
});
});
test('canonical-path', function() {
assert.equal(Gerrit.CANONICAL_PATH, '/abc/def/ghi');
});
});
</script>