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 414659c792)
This commit is contained in:
committed by
Paladox none
parent
178b7174cc
commit
558a2afbd2
@@ -2059,6 +2059,10 @@ Setting this option to true will prevent this behavior.
|
|||||||
+
|
+
|
||||||
By default false.
|
By default false.
|
||||||
|
|
||||||
|
[[gerrit.cdnPath]]gerrit.cdnPath::
|
||||||
|
+
|
||||||
|
Path prefix for PolyGerrit's static resources if using a CDN.
|
||||||
|
|
||||||
[[gitweb]]
|
[[gitweb]]
|
||||||
=== Section gitweb
|
=== Section gitweb
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ java_library(
|
|||||||
"//lib:jsch",
|
"//lib:jsch",
|
||||||
"//lib:mime-util",
|
"//lib:mime-util",
|
||||||
"//lib:servlet-api-3_1",
|
"//lib:servlet-api-3_1",
|
||||||
|
"//lib:soy",
|
||||||
"//lib/auto:auto-value",
|
"//lib/auto:auto-value",
|
||||||
"//lib/commons:codec",
|
"//lib/commons:codec",
|
||||||
"//lib/guice",
|
"//lib/guice",
|
||||||
@@ -54,6 +55,7 @@ junit_tests(
|
|||||||
srcs = glob(["src/test/java/**/*.java"]),
|
srcs = glob(["src/test/java/**/*.java"]),
|
||||||
deps = [
|
deps = [
|
||||||
":httpd",
|
":httpd",
|
||||||
|
"//gerrit-common:annotations",
|
||||||
"//gerrit-common:server",
|
"//gerrit-common:server",
|
||||||
"//gerrit-extension-api:api",
|
"//gerrit-extension-api:api",
|
||||||
"//gerrit-reviewdb:server",
|
"//gerrit-reviewdb:server",
|
||||||
@@ -66,6 +68,7 @@ junit_tests(
|
|||||||
"//lib:jimfs",
|
"//lib:jimfs",
|
||||||
"//lib:junit",
|
"//lib:junit",
|
||||||
"//lib:servlet-api-3_1-without-neverlink",
|
"//lib:servlet-api-3_1-without-neverlink",
|
||||||
|
"//lib:soy",
|
||||||
"//lib:truth",
|
"//lib:truth",
|
||||||
"//lib/easymock",
|
"//lib/easymock",
|
||||||
"//lib/guice",
|
"//lib/guice",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,11 +27,13 @@ import com.google.gerrit.httpd.XsrfCookieFilter;
|
|||||||
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
|
import com.google.gerrit.httpd.raw.ResourceServlet.Resource;
|
||||||
import com.google.gerrit.launcher.GerritLauncher;
|
import com.google.gerrit.launcher.GerritLauncher;
|
||||||
import com.google.gerrit.server.cache.CacheModule;
|
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.GerritOptions;
|
||||||
import com.google.gerrit.server.config.GerritServerConfig;
|
import com.google.gerrit.server.config.GerritServerConfig;
|
||||||
import com.google.gerrit.server.config.SitePaths;
|
import com.google.gerrit.server.config.SitePaths;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Key;
|
import com.google.inject.Key;
|
||||||
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Provides;
|
import com.google.inject.Provides;
|
||||||
import com.google.inject.ProvisionException;
|
import com.google.inject.ProvisionException;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
@@ -41,6 +43,7 @@ import com.google.inject.servlet.ServletModule;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.file.FileSystem;
|
import java.nio.file.FileSystem;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
@@ -249,9 +252,10 @@ public class StaticModule extends ServletModule {
|
|||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@Named(POLYGERRIT_INDEX_SERVLET)
|
@Named(POLYGERRIT_INDEX_SERVLET)
|
||||||
HttpServlet getPolyGerritUiIndexServlet(@Named(CACHE) Cache<Path, Resource> cache) {
|
HttpServlet getPolyGerritUiIndexServlet(@CanonicalWebUrl @Nullable String canonicalUrl,
|
||||||
return new SingleFileServlet(
|
@GerritServerConfig Config cfg) throws URISyntaxException {
|
||||||
cache, polyGerritBasePath().resolve("index.html"), getPaths().isDev(), false);
|
String cdnPath = cfg.getString("gerrit", null, "cdnPath");
|
||||||
|
return new IndexServlet(canonicalUrl, cdnPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
|
|||||||
@@ -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}
|
||||||
@@ -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/");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,12 @@
|
|||||||
value: function() { return document.body; },
|
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: {
|
_account: {
|
||||||
type: Object,
|
type: Object,
|
||||||
observer: '_accountChanged',
|
observer: '_accountChanged',
|
||||||
@@ -73,8 +79,11 @@
|
|||||||
'?': '_showKeyboardShortcuts',
|
'?': '_showKeyboardShortcuts',
|
||||||
},
|
},
|
||||||
|
|
||||||
attached: function() {
|
ready: function() {
|
||||||
|
Gerrit.CANONICAL_PATH = this.canonicalPath;
|
||||||
|
|
||||||
this.$.router.start();
|
this.$.router.start();
|
||||||
|
|
||||||
this.$.restAPI.getAccount().then(function(account) {
|
this.$.restAPI.getAccount().then(function(account) {
|
||||||
this._account = account;
|
this._account = account;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
@@ -84,9 +93,7 @@
|
|||||||
this.$.restAPI.getVersion().then(function(version) {
|
this.$.restAPI.getVersion().then(function(version) {
|
||||||
this._version = version;
|
this._version = version;
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
|
||||||
|
|
||||||
ready: function() {
|
|
||||||
this.$.reporting.appStarted();
|
this.$.reporting.appStarted();
|
||||||
this._viewState = {
|
this._viewState = {
|
||||||
changeView: {
|
changeView: {
|
||||||
@@ -108,6 +115,8 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
_accountChanged: function(account) {
|
_accountChanged: function(account) {
|
||||||
|
if (!account) { return; }
|
||||||
|
|
||||||
// Preferences are cached when a user is logged in; warm them.
|
// Preferences are cached when a user is logged in; warm them.
|
||||||
this.$.restAPI.getPreferences();
|
this.$.restAPI.getPreferences();
|
||||||
this.$.restAPI.getDiffPreferences();
|
this.$.restAPI.getDiffPreferences();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ limitations under the License.
|
|||||||
|
|
||||||
<test-fixture id="basic">
|
<test-fixture id="basic">
|
||||||
<template>
|
<template>
|
||||||
<gr-app id="app"></gr-app>
|
<gr-app id="app" canonical-path="/abc/def/ghi"></gr-app>
|
||||||
</template>
|
</template>
|
||||||
</test-fixture>
|
</test-fixture>
|
||||||
|
|
||||||
@@ -39,6 +39,9 @@ limitations under the License.
|
|||||||
stub('gr-reporting', {
|
stub('gr-reporting', {
|
||||||
appStarted: sandbox.stub(),
|
appStarted: sandbox.stub(),
|
||||||
});
|
});
|
||||||
|
stub('gr-account-dropdown', {
|
||||||
|
_getTopContent: sinon.stub(),
|
||||||
|
});
|
||||||
stub('gr-rest-api-interface', {
|
stub('gr-rest-api-interface', {
|
||||||
getAccount: function() { return Promise.resolve(null); },
|
getAccount: function() { return Promise.resolve(null); },
|
||||||
getConfig: function() {
|
getConfig: function() {
|
||||||
@@ -94,5 +97,9 @@ limitations under the License.
|
|||||||
element._loadPlugins([]);
|
element._loadPlugins([]);
|
||||||
assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
|
assert.isTrue(Gerrit._setPluginsCount.calledWithExactly(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('canonical-path', function() {
|
||||||
|
assert.equal(Gerrit.CANONICAL_PATH, '/abc/def/ghi');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user