diff --git a/docs/development/core/development-basepath.asciidoc b/docs/development/core/development-basepath.asciidoc index b7e0dec88bf50..2da6507935015 100644 --- a/docs/development/core/development-basepath.asciidoc +++ b/docs/development/core/development-basepath.asciidoc @@ -48,15 +48,15 @@ $http.get(chrome.addBasePath('/api/plugin/things')); [float] ==== Server side -Append `config.get('server.basePath')` to any absolute URL path. +Append `request.getBasePath()` to any absolute URL path. ["source","shell"] ----------- const basePath = server.config().get('server.basePath'); server.route({ path: '/redirect', - handler(req, reply) { - reply.redirect(`${basePath}/otherLocation`); + handler(request, reply) { + reply.redirect(`${request.getBasePath()}/otherLocation`); } }); ----------- diff --git a/src/server/http/__snapshots__/setup_base_path_provider.test.js.snap b/src/server/http/__snapshots__/setup_base_path_provider.test.js.snap new file mode 100644 index 0000000000000..9bc81d1b2efab --- /dev/null +++ b/src/server/http/__snapshots__/setup_base_path_provider.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`setupBasePathProvider getBasePath/setBasePath should not allow request.setBasePath to be called more than once 1`] = `"Request basePath was previously set. Setting multiple times is not supported."`; diff --git a/src/server/http/index.js b/src/server/http/index.js index 7012b095a8658..d7a79b0d02fa7 100644 --- a/src/server/http/index.js +++ b/src/server/http/index.js @@ -24,6 +24,7 @@ import Boom from 'boom'; import Hapi from 'hapi'; import { setupVersionCheck } from './version_check'; import { registerHapiPlugins } from './register_hapi_plugins'; +import { setupBasePathProvider } from './setup_base_path_provider'; import { setupXsrf } from './xsrf'; export default async function (kbnServer, server, config) { @@ -32,6 +33,8 @@ export default async function (kbnServer, server, config) { server.connection(kbnServer.core.serverOptions); + setupBasePathProvider(server, config); + registerHapiPlugins(server); // provide a simple way to expose static directories @@ -86,7 +89,7 @@ export default async function (kbnServer, server, config) { path: '/', method: 'GET', handler(req, reply) { - const basePath = config.get('server.basePath'); + const basePath = req.getBasePath(); const defaultRoute = config.get('server.defaultRoute'); reply.redirect(`${basePath}${defaultRoute}`); } @@ -100,7 +103,7 @@ export default async function (kbnServer, server, config) { if (path === '/' || path.charAt(path.length - 1) !== '/') { return reply(Boom.notFound()); } - const pathPrefix = config.get('server.basePath') ? `${config.get('server.basePath')}/` : ''; + const pathPrefix = req.getBasePath() ? `${req.getBasePath()}/` : ''; return reply.redirect(format({ search: req.url.search, pathname: pathPrefix + path.slice(0, -1), diff --git a/src/server/http/setup_base_path_provider.js b/src/server/http/setup_base_path_provider.js new file mode 100644 index 0000000000000..1df19a8c296d9 --- /dev/null +++ b/src/server/http/setup_base_path_provider.js @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ + +export function setupBasePathProvider(server, config) { + + server.decorate('request', 'setBasePath', function (basePath) { + const request = this; + if (request.app._basePath) { + throw new Error(`Request basePath was previously set. Setting multiple times is not supported.`); + } + request.app._basePath = basePath; + }); + + server.decorate('request', 'getBasePath', function () { + const request = this; + + const serverBasePath = config.get('server.basePath'); + const requestBasePath = request.app._basePath || ''; + + return `${serverBasePath}${requestBasePath}`; + }); +} \ No newline at end of file diff --git a/src/server/http/setup_base_path_provider.test.js b/src/server/http/setup_base_path_provider.test.js new file mode 100644 index 0000000000000..38fc6ba07384c --- /dev/null +++ b/src/server/http/setup_base_path_provider.test.js @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you 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. + */ +import { Server } from 'hapi'; +import { setupBasePathProvider } from './setup_base_path_provider'; + +describe('setupBasePathProvider', () => { + it('registers two request decorations', () => { + const configValues = {}; + + const server = { + decorate: jest.fn() + }; + + const config = { + get: jest.fn(() => configValues) + }; + + setupBasePathProvider(server, config); + + expect(server.decorate).toHaveBeenCalledTimes(2); + expect(server.decorate).toHaveBeenCalledWith('request', 'getBasePath', expect.any(Function)); + expect(server.decorate).toHaveBeenCalledWith('request', 'setBasePath', expect.any(Function)); + }); + + describe('getBasePath/setBasePath', () => { + + const defaultSetupFn = () => { + return; + }; + + let request; + const teardowns = []; + + beforeEach(() => { + request = async (url, serverConfig = {}, setupFn = defaultSetupFn) => { + const server = new Server(); + server.connection({ port: 0 }); + + server.route({ + path: '/', + method: 'GET', + handler(req, reply) { + setupFn(req, reply); + return reply({ basePath: req.getBasePath() }); + } + }); + + setupBasePathProvider(server, { + get: (key) => serverConfig[key] + }); + + teardowns.push(() => server.stop()); + + return server.inject({ + method: 'GET', + url, + }); + }; + }); + + afterEach(async () => { + await Promise.all(teardowns.splice(0).map(fn => fn())); + }); + + + it('should return an empty string when server.basePath is not set', async () => { + const response = await request('/', { + ['server.basePath']: '' + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + expect(JSON.parse(payload)).toEqual({ + basePath: '' + }); + }); + + it('should return the server.basePath unmodified when request.setBasePath is not called', async () => { + const response = await request('/', { + ['server.basePath']: '/path' + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + expect(JSON.parse(payload)).toEqual({ + basePath: '/path' + }); + }); + + it('should add the request basePath to the server.basePath', async () => { + const response = await request('/', { + ['server.basePath']: '/path' + }, (req) => { + req.setBasePath('/request/base/path'); + }); + + const { statusCode, payload } = response; + + expect(statusCode).toEqual(200); + expect(JSON.parse(payload)).toEqual({ + basePath: '/path/request/base/path' + }); + }); + + it('should not allow request.setBasePath to be called more than once', async () => { + const response = request('/', { + ['server.basePath']: '/path' + }, (req) => { + req.setBasePath('/request/base/path'); + req.setBasePath('/request/base/path/again'); + }); + + expect(response).rejects.toThrowErrorMatchingSnapshot(); + }); + }); +}); \ No newline at end of file diff --git a/src/ui/public/chrome/api/__tests__/nav.js b/src/ui/public/chrome/api/__tests__/nav.js index 169c9546a4a37..1ca5f2689590d 100644 --- a/src/ui/public/chrome/api/__tests__/nav.js +++ b/src/ui/public/chrome/api/__tests__/nav.js @@ -27,6 +27,7 @@ const basePath = '/someBasePath'; function init(customInternals = { basePath }) { const chrome = { + addBasePath: (path) => path, getBasePath: () => customInternals.basePath || '', }; const internals = { @@ -39,7 +40,7 @@ function init(customInternals = { basePath }) { describe('chrome nav apis', function () { describe('#getNavLinkById', () => { - it ('retrieves the correct nav link, given its ID', () => { + it('retrieves the correct nav link, given its ID', () => { const appUrlStore = new StubBrowserStorage(); const nav = [ { id: 'kibana:discover', title: 'Discover' } @@ -52,7 +53,7 @@ describe('chrome nav apis', function () { expect(navLink).to.eql(nav[0]); }); - it ('throws an error if the nav link with the given ID is not found', () => { + it('throws an error if the nav link with the given ID is not found', () => { const appUrlStore = new StubBrowserStorage(); const nav = [ { id: 'kibana:discover', title: 'Discover' } diff --git a/src/ui/public/chrome/api/nav.js b/src/ui/public/chrome/api/nav.js index 04aab308396c4..329d1c463884d 100644 --- a/src/ui/public/chrome/api/nav.js +++ b/src/ui/public/chrome/api/nav.js @@ -131,8 +131,8 @@ export function initChromeNavApi(chrome, internals) { }; internals.nav.forEach(link => { - link.url = relativeToAbsolute(link.url); - link.subUrlBase = relativeToAbsolute(link.subUrlBase); + link.url = relativeToAbsolute(chrome.addBasePath(link.url)); + link.subUrlBase = relativeToAbsolute(chrome.addBasePath(link.subUrlBase)); }); // simulate a possible change in url to initialize the diff --git a/src/ui/ui_apps/ui_app.js b/src/ui/ui_apps/ui_app.js index a4c1f4a15a27e..431667589bb2b 100644 --- a/src/ui/ui_apps/ui_app.js +++ b/src/ui/ui_apps/ui_app.js @@ -63,7 +63,7 @@ export class UiApp { // unless an app is hidden it gets a navlink, but we only respond to `getNavLink()` // if the app is also listed. This means that all apps in the kibanaPayload will // have a navLink property since that list includes all normally accessible apps - this._navLink = new UiNavLink(kbnServer.config.get('server.basePath'), { + this._navLink = new UiNavLink({ id: this._id, title: this._title, order: this._order, diff --git a/src/ui/ui_nav_links/__tests__/ui_nav_link.js b/src/ui/ui_nav_links/__tests__/ui_nav_link.js index 0cac763473146..6ac7bf55d1826 100644 --- a/src/ui/ui_nav_links/__tests__/ui_nav_link.js +++ b/src/ui/ui_nav_links/__tests__/ui_nav_link.js @@ -24,7 +24,6 @@ import { UiNavLink } from '../ui_nav_link'; describe('UiNavLink', () => { describe('constructor', () => { it('initializes the object properties as expected', () => { - const urlBasePath = 'http://localhost:5601/rnd'; const spec = { id: 'kibana:discover', title: 'Discover', @@ -36,13 +35,13 @@ describe('UiNavLink', () => { disabled: true }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.eql({ id: spec.id, title: spec.title, order: spec.order, - url: `${urlBasePath}${spec.url}`, - subUrlBase: `${urlBasePath}${spec.url}`, + url: spec.url, + subUrlBase: spec.url, description: spec.description, icon: spec.icon, hidden: spec.hidden, @@ -54,22 +53,7 @@ describe('UiNavLink', () => { }); }); - it('initializes the url property without a base path when one is not specified in the spec', () => { - const urlBasePath = undefined; - const spec = { - id: 'kibana:discover', - title: 'Discover', - order: -1003, - url: '/app/kibana#/discover', - description: 'interactively explore your data', - icon: 'plugins/kibana/assets/discover.svg', - }; - const link = new UiNavLink(urlBasePath, spec); - expect(link.toJSON()).to.have.property('url', spec.url); - }); - it('initializes the order property to 0 when order is not specified in the spec', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -77,13 +61,12 @@ describe('UiNavLink', () => { description: 'interactively explore your data', icon: 'plugins/kibana/assets/discover.svg', }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('order', 0); }); it('initializes the linkToLastSubUrl property to false when false is specified in the spec', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -93,13 +76,12 @@ describe('UiNavLink', () => { icon: 'plugins/kibana/assets/discover.svg', linkToLastSubUrl: false }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('linkToLastSubUrl', false); }); it('initializes the linkToLastSubUrl property to true by default', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -108,13 +90,12 @@ describe('UiNavLink', () => { description: 'interactively explore your data', icon: 'plugins/kibana/assets/discover.svg', }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('linkToLastSubUrl', true); }); it('initializes the hidden property to false by default', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -123,13 +104,12 @@ describe('UiNavLink', () => { description: 'interactively explore your data', icon: 'plugins/kibana/assets/discover.svg', }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('hidden', false); }); it('initializes the disabled property to false by default', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -138,13 +118,12 @@ describe('UiNavLink', () => { description: 'interactively explore your data', icon: 'plugins/kibana/assets/discover.svg', }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('disabled', false); }); it('initializes the tooltip property to an empty string by default', () => { - const urlBasePath = undefined; const spec = { id: 'kibana:discover', title: 'Discover', @@ -153,7 +132,7 @@ describe('UiNavLink', () => { description: 'interactively explore your data', icon: 'plugins/kibana/assets/discover.svg', }; - const link = new UiNavLink(urlBasePath, spec); + const link = new UiNavLink(spec); expect(link.toJSON()).to.have.property('tooltip', ''); }); diff --git a/src/ui/ui_nav_links/ui_nav_link.js b/src/ui/ui_nav_links/ui_nav_link.js index 8ecf6b2cb6782..fe2d7c84b40a1 100644 --- a/src/ui/ui_nav_links/ui_nav_link.js +++ b/src/ui/ui_nav_links/ui_nav_link.js @@ -18,7 +18,7 @@ */ export class UiNavLink { - constructor(urlBasePath, spec) { + constructor(spec) { const { id, title, @@ -36,8 +36,8 @@ export class UiNavLink { this._id = id; this._title = title; this._order = order; - this._url = `${urlBasePath || ''}${url}`; - this._subUrlBase = `${urlBasePath || ''}${subUrlBase || url}`; + this._url = url; + this._subUrlBase = subUrlBase || url; this._description = description; this._icon = icon; this._linkToLastSubUrl = linkToLastSubUrl; diff --git a/src/ui/ui_nav_links/ui_nav_links_mixin.js b/src/ui/ui_nav_links/ui_nav_links_mixin.js index 2c94135a113e7..ef51000a3b0af 100644 --- a/src/ui/ui_nav_links/ui_nav_links_mixin.js +++ b/src/ui/ui_nav_links/ui_nav_links_mixin.js @@ -19,14 +19,13 @@ import { UiNavLink } from './ui_nav_link'; -export function uiNavLinksMixin(kbnServer, server, config) { +export function uiNavLinksMixin(kbnServer, server) { const uiApps = server.getAllUiApps(); const { navLinkSpecs = [] } = kbnServer.uiExports; - const urlBasePath = config.get('server.basePath'); const fromSpecs = navLinkSpecs - .map(navLinkSpec => new UiNavLink(urlBasePath, navLinkSpec)); + .map(navLinkSpec => new UiNavLink(navLinkSpec)); const fromApps = uiApps .map(app => app.getNavLink()) diff --git a/src/ui/ui_render/ui_render_mixin.js b/src/ui/ui_render/ui_render_mixin.js index f94781cfddf04..f655fe414d3ba 100644 --- a/src/ui/ui_render/ui_render_mixin.js +++ b/src/ui/ui_render/ui_render_mixin.js @@ -117,7 +117,7 @@ export function uiRenderMixin(kbnServer, server, config) { branch: config.get('pkg.branch'), buildNum: config.get('pkg.buildNum'), buildSha: config.get('pkg.buildSha'), - basePath: config.get('server.basePath'), + basePath: request.getBasePath(), serverName: config.get('server.name'), devMode: config.get('env.dev'), uiSettings: await props({ @@ -131,7 +131,7 @@ export function uiRenderMixin(kbnServer, server, config) { try { const request = reply.request; const translations = await server.getUiTranslations(); - const basePath = config.get('server.basePath'); + const basePath = request.getBasePath(); return reply.view('ui_app', { uiPublicUrl: `${basePath}/ui`,