-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding an app for redirects when storing state in session storage (#1…
…0822) (#11305) * Adding an app for redirects when storing state in session storage * Removing errant console.log * Adding early return after reply().redirect * No longer using the router * Renaming vars to injectedVarsOverrides * Putting uiRoutes back in so the code is only executed for this app * Extracting hash_url to it's own module, and adding tests * Addressing peer-review comments
- Loading branch information
Showing
9 changed files
with
279 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export default function (kibana) { | ||
return new kibana.Plugin({ | ||
uiExports: { | ||
app: { | ||
require: ['kibana'], | ||
title: 'Redirecting', | ||
id: 'stateSessionStorageRedirect', | ||
main: 'plugins/state_session_storage_redirect', | ||
listed: false, | ||
} | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"name": "state_session_storage_redirect", | ||
"version": "kibana", | ||
"description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the kbn-initial-state and and put the URL hashed states into sessionStorage before redirecting the user." | ||
} |
19 changes: 19 additions & 0 deletions
19
src/core_plugins/state_session_storage_redirect/public/index.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import 'ui/autoload/styles'; | ||
import chrome from 'ui/chrome'; | ||
import { hashUrl } from 'ui/state_management/state_hashing'; | ||
import uiRoutes from 'ui/routes'; | ||
|
||
uiRoutes.enable(); | ||
uiRoutes | ||
.when('/', { | ||
resolve: { | ||
url: function (AppState, globalState, $window) { | ||
const redirectUrl = chrome.getInjected('redirectUrl'); | ||
|
||
const hashedUrl = hashUrl([new AppState(), globalState], redirectUrl); | ||
const url = chrome.addBasePath(hashedUrl); | ||
|
||
$window.location = url; | ||
} | ||
} | ||
}); |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
144 changes: 144 additions & 0 deletions
144
src/ui/public/state_management/state_hashing/__tests__/hash_url.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,144 @@ | ||
import expect from 'expect.js'; | ||
import ngMock from 'ng_mock'; | ||
import sinon from 'auto-release-sinon'; | ||
import { parse as parseUrl } from 'url'; | ||
|
||
import StateProvider from 'ui/state_management/state'; | ||
import { hashUrl } from 'ui/state_management/state_hashing'; | ||
|
||
describe('hashUrl', function () { | ||
let State; | ||
|
||
beforeEach(ngMock.module('kibana')); | ||
|
||
beforeEach(ngMock.inject((Private, config) => { | ||
State = Private(StateProvider); | ||
sinon.stub(config, 'get').withArgs('state:storeInSessionStorage').returns(true); | ||
})); | ||
|
||
describe('throws error', () => { | ||
it('if states parameter is null', () => { | ||
expect(() => { | ||
hashUrl(null, ''); | ||
}).to.throwError(); | ||
}); | ||
|
||
it('if states parameter is empty array', () => { | ||
expect(() => { | ||
hashUrl([], ''); | ||
}).to.throwError(); | ||
}); | ||
}); | ||
|
||
describe('does nothing', () => { | ||
let states; | ||
beforeEach(() => { | ||
states = [new State('testParam')]; | ||
}); | ||
it('if url is empty', () => { | ||
const url = ''; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if just a host and port', () => { | ||
const url = 'https://localhost:5601'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if just a path', () => { | ||
const url = 'https://localhost:5601/app/kibana'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if just a path and query', () => { | ||
const url = 'https://localhost:5601/app/kibana?foo=bar'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if empty hash with query', () => { | ||
const url = 'https://localhost:5601/app/kibana?foo=bar#'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if query parameter matches and there is no hash', () => { | ||
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it(`if query parameter matches and it's before the hash`, () => { | ||
const url = 'https://localhost:5601/app/kibana?testParam=(yes:!t)'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if empty hash without query', () => { | ||
const url = 'https://localhost:5601/app/kibana#'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if empty hash without query', () => { | ||
const url = 'https://localhost:5601/app/kibana#'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if hash is just a path', () => { | ||
const url = 'https://localhost:5601/app/kibana#/discover'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
|
||
it('if hash does not have matching query string vals', () => { | ||
const url = 'https://localhost:5601/app/kibana#/discover?foo=bar'; | ||
expect(hashUrl(states, url)).to.be(url); | ||
}); | ||
}); | ||
|
||
describe('replaces querystring value with hash', () => { | ||
const getAppQuery = (url) => { | ||
const parsedUrl = parseUrl(url); | ||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); | ||
|
||
return parsedAppUrl.query; | ||
}; | ||
|
||
it('if using a single State', () => { | ||
const stateParamKey = 'testParam'; | ||
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey}=(yes:!t)`; | ||
const mockHashedItemStore = { | ||
getItem: () => null, | ||
setItem: sinon.stub().returns(true) | ||
}; | ||
const state = new State(stateParamKey, {}, mockHashedItemStore); | ||
|
||
const actualUrl = hashUrl([state], url); | ||
|
||
expect(mockHashedItemStore.setItem.calledOnce).to.be(true); | ||
|
||
const appQuery = getAppQuery(actualUrl); | ||
|
||
const hashKey = mockHashedItemStore.setItem.firstCall.args[0]; | ||
expect(appQuery[stateParamKey]).to.eql(hashKey); | ||
}); | ||
|
||
it('if using multiple States', () => { | ||
const stateParamKey1 = 'testParam1'; | ||
const stateParamKey2 = 'testParam2'; | ||
const url = `https://localhost:5601/app/kibana#/discover?foo=bar&${stateParamKey1}=(yes:!t)&${stateParamKey2}=(yes:!f)`; | ||
const mockHashedItemStore = { | ||
getItem: () => null, | ||
setItem: sinon.stub().returns(true) | ||
}; | ||
const state1 = new State(stateParamKey1, {}, mockHashedItemStore); | ||
const state2 = new State(stateParamKey2, {}, mockHashedItemStore); | ||
|
||
const actualUrl = hashUrl([state1, state2], url); | ||
|
||
expect(mockHashedItemStore.setItem.calledTwice).to.be(true); | ||
|
||
const appQuery = getAppQuery(actualUrl); | ||
|
||
const hashKey1 = mockHashedItemStore.setItem.firstCall.args[0]; | ||
const hashKey2 = mockHashedItemStore.setItem.secondCall.args[0]; | ||
expect(appQuery[stateParamKey1]).to.eql(hashKey1); | ||
expect(appQuery[stateParamKey2]).to.eql(hashKey2); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import encodeUriQuery from 'encode-uri-query'; | ||
import rison from 'rison-node'; | ||
import { parse as parseUrl, format as formatUrl } from 'url'; | ||
import { stringify as stringifyQuerystring } from 'querystring'; | ||
|
||
const conservativeStringifyQuerystring = (query) => { | ||
return stringifyQuerystring(query, null, null, { | ||
encodeURIComponent: encodeUriQuery | ||
}); | ||
}; | ||
|
||
const hashStateInQuery = (state, query) => { | ||
const name = state.getQueryParamName(); | ||
const value = query[name]; | ||
if (!value) { | ||
return { name, value }; | ||
} | ||
|
||
const decodedValue = rison.decode(value); | ||
const hashedValue = state.toQueryParam(decodedValue); | ||
return { name, value: hashedValue }; | ||
}; | ||
|
||
const hashStatesInQuery = (states, query) => { | ||
const hashedQuery = states.reduce((result, state) => { | ||
const { name, value } = hashStateInQuery(state, query); | ||
if (value) { | ||
result[name] = value; | ||
} | ||
return result; | ||
}, {}); | ||
|
||
|
||
return Object.assign({}, query, hashedQuery); | ||
}; | ||
|
||
export const hashUrl = (states, redirectUrl) => { | ||
// we need states to proceed, throwing an error if we don't have any | ||
if (states === null || !states.length) { | ||
throw new Error('states parameter must be an Array with length greater than 0'); | ||
} | ||
|
||
const parsedUrl = parseUrl(redirectUrl); | ||
// if we don't have a hash, we return the redirectUrl without hashing anything | ||
if (!parsedUrl.hash) { | ||
return redirectUrl; | ||
} | ||
|
||
// The URLs that we use aren't "conventional" and the hash is sometimes appearing before | ||
// the querystring, even though conventionally they appear after it. The parsedUrl | ||
// is the entire URL, and the parsedAppUrl is everything after the hash. | ||
// | ||
// EXAMPLE | ||
// parsedUrl: /app/kibana#/visualize/edit/somelongguid?g=()&a=() | ||
// parsedAppUrl: /visualize/edit/somelongguid?g=()&a=() | ||
const parsedAppUrl = parseUrl(parsedUrl.hash.slice(1), true); | ||
|
||
// the parsedAppUrl actually has the query that we care about | ||
const query = parsedAppUrl.query; | ||
|
||
const newQuery = hashStatesInQuery(states, query); | ||
|
||
const newHash = formatUrl({ | ||
search: conservativeStringifyQuerystring(newQuery), | ||
pathname: parsedAppUrl.pathname | ||
}); | ||
|
||
return formatUrl({ | ||
hash: `#${newHash}`, | ||
host: parsedUrl.host, | ||
search: parsedUrl.search, | ||
pathname: parsedUrl.pathname, | ||
protocol: parsedUrl.protocol, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters