Commit d017358b authored by Robert Knight's avatar Robert Knight

Add shared module for config fragment generation and parsing

The logic for generating the `#config=<URI-encoded JSON blob>` fragment,
that is used to pass configuration from the annotator to the sidebar
application, was duplicated in several places in the code.

Add a shared module in `shared/config-fragment.js` with functions for
adding configuration to and extracting it from the URL.
parent 8dd27426
......@@ -2,6 +2,7 @@ import { IconButton } from '@hypothesis/frontend-shared';
import { useEffect, useRef, useState } from 'preact/hooks';
import classnames from 'classnames';
import { addConfigFragment } from '../../shared/config-fragment';
import { createSidebarConfig } from '../config/sidebar';
/**
......@@ -16,11 +17,12 @@ import { createSidebarConfig } from '../config/sidebar';
* @param {NotebookIframeProps} props
*/
function NotebookIframe({ config, groupId }) {
const notebookConfig = createSidebarConfig(config);
// Explicity set the "focused" group
notebookConfig.group = groupId;
const configParam = encodeURIComponent(JSON.stringify(notebookConfig));
const notebookAppSrc = `${config.notebookAppUrl}#config=${configParam}`;
const notebookAppSrc = addConfigFragment(config.notebookAppUrl, {
...createSidebarConfig(config),
// Explicity set the "focused" group
group: groupId,
});
return (
<iframe
......
import { act } from 'preact/test-utils';
import { mount } from 'enzyme';
import NotebookModal from '../NotebookModal';
import { addConfigFragment } from '../../../shared/config-fragment';
import { EventBus } from '../../util/emitter';
import NotebookModal from '../NotebookModal';
describe('NotebookModal', () => {
const notebookURL = 'https://test.hypothes.is/notebook';
let components;
let eventBus;
let emitter;
......@@ -13,7 +16,7 @@ describe('NotebookModal', () => {
const component = mount(
<NotebookModal
eventBus={eventBus}
config={{ notebookAppUrl: '/notebook', ...config }}
config={{ notebookAppUrl: notebookURL, ...config }}
/>
);
components.push(component);
......@@ -53,7 +56,7 @@ describe('NotebookModal', () => {
const iframe = wrapper.find('iframe');
assert.equal(
iframe.prop('src'),
`/notebook#config=${encodeURIComponent('{"group":"myGroup"}')}`
addConfigFragment(notebookURL, { group: 'myGroup' })
);
});
......@@ -66,7 +69,7 @@ describe('NotebookModal', () => {
const iframe1 = wrapper.find('iframe');
assert.equal(
iframe1.prop('src'),
`/notebook#config=${encodeURIComponent('{"group":"1"}')}`
addConfigFragment(notebookURL, { group: '1' })
);
emitter.publish('openNotebook', '1');
......@@ -75,7 +78,7 @@ describe('NotebookModal', () => {
const iframe2 = wrapper.find('iframe');
assert.equal(
iframe2.prop('src'),
`/notebook#config=${encodeURIComponent('{"group":"1"}')}`
addConfigFragment(notebookURL, { group: '1' })
);
assert.notEqual(iframe1.getDOMNode(), iframe2.getDOMNode());
......@@ -85,7 +88,7 @@ describe('NotebookModal', () => {
const iframe3 = wrapper.find('iframe');
assert.equal(
iframe3.prop('src'),
`/notebook#config=${encodeURIComponent('{"group":"2"}')}`
addConfigFragment(notebookURL, { group: '2' })
);
assert.notEqual(iframe1.getDOMNode(), iframe3.getDOMNode());
});
......
import Hammer from 'hammerjs';
import { Bridge } from '../shared/bridge';
import { addConfigFragment } from '../shared/config-fragment';
import { ListenerCollection } from '../shared/listener-collection';
import { annotationCounts } from './annotation-counts';
......@@ -28,10 +29,10 @@ export const MIN_RESIZE = 280;
* @return {HTMLIFrameElement}
*/
function createSidebarIframe(config) {
const sidebarConfig = createSidebarConfig(config);
const configParam =
'config=' + encodeURIComponent(JSON.stringify(sidebarConfig));
const sidebarAppSrc = config.sidebarAppUrl + '#' + configParam;
const sidebarAppSrc = addConfigFragment(
config.sidebarAppUrl,
createSidebarConfig(config)
);
const sidebarFrame = document.createElement('iframe');
......
import { addConfigFragment } from '../../shared/config-fragment';
import Sidebar, { MIN_RESIZE, $imports } from '../sidebar';
import { EventBus } from '../util/emitter';
......@@ -6,6 +7,11 @@ const DEFAULT_HEIGHT = 600;
const EXTERNAL_CONTAINER_SELECTOR = 'test-external-container';
describe('Sidebar', () => {
const sidebarURL = new URL(
'/base/annotator/test/empty.html',
window.location.href
).toString();
const sandbox = sinon.createSandbox();
// Containers and Sidebar instances created by current test.
......@@ -40,7 +46,7 @@ describe('Sidebar', () => {
const createSidebar = (config = {}) => {
config = {
// Dummy sidebar app.
sidebarAppUrl: '/base/annotator/test/empty.html',
sidebarAppUrl: sidebarURL,
...config,
};
const container = document.createElement('div');
......@@ -176,19 +182,11 @@ describe('Sidebar', () => {
return sidebar.iframe.src;
}
function configFragment(config) {
return '#config=' + encodeURIComponent(JSON.stringify(config));
}
it('creates sidebar iframe and passes configuration to it', () => {
const appURL = new URL(
'/base/annotator/test/empty.html',
window.location.href
);
const sidebar = createSidebar({ annotations: '1234' });
assert.equal(
getConfigString(sidebar),
appURL + configFragment({ annotations: '1234' })
addConfigFragment(sidebarURL, { annotations: '1234' })
);
});
......
/**
* Encode app configuration in a URL fragment.
*
* This is used by the annotator to pass configuration to the sidebar and
* notebook apps, which they can easily read on startup. The configuration is
* passed in the fragment to avoid invalidating cache entries for the URL
* or adding noise to server logs.
*
* @param {string} baseURL
* @param {object} config
* @return {string} URL with added fragment
*/
export function addConfigFragment(baseURL, config) {
const url = new URL(baseURL);
const params = new URLSearchParams();
params.append('config', JSON.stringify(config));
url.hash = params.toString();
return url.toString();
}
/**
* Parse configuration from a URL generated by {@link addConfigFragment}.
*
* @param {string} url
* @return {object}
*/
export function parseConfigFragment(url) {
const configStr = new URL(url).hash.slice(1);
const configJSON = new URLSearchParams(configStr).get('config');
return JSON.parse(configJSON || '{}');
}
import { addConfigFragment, parseConfigFragment } from '../config-fragment';
describe('shared/config-fragment', () => {
describe('addConfigFragment', () => {
it('returns URL with added config fragment', () => {
const config = { appType: 'bar' };
const url = addConfigFragment('https://example.com/app.html', config);
assert.equal(
url,
'https://example.com/app.html#config=%7B%22appType%22%3A%22bar%22%7D'
);
});
it('replaces any existing fragment', () => {
const config = { appType: 'bar' };
const url = addConfigFragment('https://example.com/app.html#foo', config);
assert.equal(
url,
'https://example.com/app.html#config=%7B%22appType%22%3A%22bar%22%7D'
);
});
});
describe('parseConfigFragment', () => {
it('parses fragment generated by `addConfigFragment`', () => {
const config = { appType: 'foo' };
const urlWithConfig = addConfigFragment(
'https://example.com/app.html',
config
);
const parsedConfig = parseConfigFragment(urlWithConfig);
assert.deepEqual(parsedConfig, config);
});
['', '#foo'].forEach(fragment => {
it('returns an empty object if the URL has no config fragment', () => {
const url = `https://example.com/app.html${fragment}`;
const parsedConfig = parseConfigFragment(url);
assert.deepEqual(parsedConfig, {});
});
});
});
});
import { parseConfigFragment } from '../../shared/config-fragment';
import {
toBoolean,
toInteger,
......@@ -11,12 +12,11 @@ import {
* Return the app configuration specified by the frame embedding the Hypothesis
* client.
*
* @param {Window} window
* @return {HostConfig}
*/
export default function hostPageConfig(window) {
const configStr = window.location.hash.slice(1);
const configJSON = new URLSearchParams(configStr).get('config');
const config = JSON.parse(configJSON || '{}');
const config = parseConfigFragment(window.location.href);
// Known configuration parameters which we will import from the host page.
// Note that since the host page is untrusted code, the filtering needs to
......
import { addConfigFragment } from '../../../shared/config-fragment';
import hostPageConfig from '../host-config';
function fakeWindow(config) {
return {
location: {
hash: '#config=' + JSON.stringify(config),
href: addConfigFragment('https://hypothes.is/app.html', config),
},
};
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment