Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __tests__/integration/mirador/window_actions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ describe('Window actions', () => {
)); // only default configed windows found
await page.waitFor(1000);
await expect(numWindows).toBe(2);
});
}, 10000);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this needed for the replacement of the iframe component? Were the tests failing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no, nothing to do with the component. these puppeteer tests consistently fail for me locally and also sometimes on CI. increasing the timeout solves it.

});
71 changes: 71 additions & 0 deletions __tests__/src/lib/IFrameComm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import { mount } from 'enzyme';
import { IframeComm } from '../../../src/lib/IFrameComm';

/**
*
* @param props
* @returns {*}
*/
function createWrapper(props) {
return mount(
<IframeComm
handleReady={() => {}}
handleReceiveMessage={() => {}}
postMessageData="this is a test"
{...props}
/>,
);
}

let receivedMessage = {};
const mockIFrameContents = {
contentWindow: {
postMessage: (postMessageData) => {
receivedMessage = postMessageData;
},
},
};

describe('IFrameComm', () => {
let wrapper;
it('renders properly', () => {
const handleReady = jest.fn();
const handleReceiveMessage = jest.fn();
wrapper = createWrapper({ handleReady, handleReceiveMessage });
expect(wrapper.find(IframeComm).length).toBe(1);
wrapper.instance().setIFrameElement(mockIFrameContents);
wrapper.instance().onLoad();
expect(handleReady).toHaveBeenCalled();
expect(receivedMessage === 'this is a test').toBeTruthy();
wrapper.instance().onReceiveMessage();
expect(handleReceiveMessage).toHaveBeenCalled();
});
it('updates props', () => {
wrapper = createWrapper();
wrapper.instance().setIFrameElement(mockIFrameContents);
wrapper.instance().onLoad();
wrapper.setProps({ postMessageData: 'this is another test' });
wrapper.instance().setIFrameElement(mockIFrameContents);
wrapper.instance().onLoad();
expect(receivedMessage === 'this is another test').toBeTruthy();
});
it('returns data with no serialization option', () => {
wrapper = createWrapper({ serializeMessage: false });
wrapper.instance().setIFrameElement(mockIFrameContents);
wrapper.instance().onLoad();
expect(receivedMessage === 'this is a test').toBeTruthy();
});
it('returns data with when payload is an object', () => {
wrapper = createWrapper({ postMessageData: { object: 'this is an object' } });
wrapper.instance().setIFrameElement(mockIFrameContents);
wrapper.instance().onLoad();
expect(receivedMessage === '{"object":"this is an object"}').toBeTruthy();
});
it('ummounts', () => {
wrapper = createWrapper();
const componentWillUnmount = jest.spyOn(wrapper.instance(), 'componentWillUnmount');
wrapper.unmount();
expect(componentWillUnmount).toHaveBeenCalled();
});
});
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"react-copy-to-clipboard": "^5.0.1",
"react-full-screen": "^0.2.4",
"react-i18next": "^10.11.4",
"react-iframe-comm": "^1.2.2",
"react-image": "^2.1.3",
"react-mosaic-component": "^3.2.0",
"react-placeholder": "^3.0.1",
Expand Down
3 changes: 2 additions & 1 deletion src/components/AccessTokenSender.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import IframeComm from 'react-iframe-comm';
import { IframeComm } from '../lib/IFrameComm';

/**
* Opens a new window for click
Expand Down Expand Up @@ -32,6 +32,7 @@ export class AccessTokenSender extends Component {
<IframeComm
attributes={{ src: `${url}?origin=${window.origin}&messageId=${url}` }}
handleReceiveMessage={this.onReceiveAccessTokenMessage}
postMessageData=""
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/WindowAuthenticationControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class WindowAuthenticationControl extends Component {
<Paper square elevation={4} color="secondary" classes={{ root: classes.paper }}>
<Button fullWidth className={classes.topBar} onClick={hasCollapsedContent ? this.handleClickOpen : this.handleConfirm} component="div" color="inherit">
<LockIcon className={classes.icon} />
<Typography className={classes.label} component="h3" variant="body1" color="inherit" inline>
<Typography className={classes.label} component="h3" variant="body1" color="inherit" inline="true">
<SanitizedHtml htmlString={(isInFailureState ? failureHeader : label) || t('authenticationRequired')} ruleSet="iiif" />
</Typography>
<span className={classes.fauxButton}>
Expand Down
225 changes: 225 additions & 0 deletions src/lib/IFrameComm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
MIT License
Copyright (c) 2017 Petar Bojinov
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

import React, { Component } from 'react';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is code copied from react-iframe-comm? We probably need to preserve the upstream license information in this file (and any others we're copying over.).

import PropTypes from 'prop-types';

/**
*
*/
export class IframeComm extends Component {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the reason for this living in src/lib because it is code copied from upstream? I would expect it to be in src/components

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lib is usually where compiled code goes so storing source in this directory is odd anyway, but in this case, since it is a "third party library component" (note also that it is a derivation, not a copy), it could/should reside in a directory that is distinct from your homegrown code (e.g. vendors) or even better, make a monorepo and build it as a separate package.

/**
*
*/
constructor() {
super();
this.onReceiveMessage = this.onReceiveMessage.bind(this);
this.onLoad = this.onLoad.bind(this);
this.sendMessage = this.sendMessage.bind(this);
this.setIFrameElement = (el) => {
this.frame = el;
};
}

/**
*
*/
componentDidMount() {
window.addEventListener('message', this.onReceiveMessage);
this.frame.addEventListener('load', this.onLoad);
}

/**
*
* @param prevProps
* @param prevState
*/
componentDidUpdate(prevProps, prevState) {
const { postMessageData } = this.props;
if (prevProps.postMessageData !== postMessageData) {
// send a message if postMessageData changed
this.sendMessage(postMessageData);
}
}

/**
*
*/
componentWillUnmount() {
window.removeEventListener('message', this.onReceiveMessage, false);
}

/**
*
* @param event
*/
onReceiveMessage(event) {
const { handleReceiveMessage } = this.props;
if (handleReceiveMessage) {
handleReceiveMessage(event);
}
}

/**
*
*/
onLoad() {
const { handleReady } = this.props;
if (handleReady) {
handleReady();
}
// TODO: Look into doing a syn-ack TCP-like handshake
// to make sure iFrame is ready to REALLY accept messages, not just loaded.
// send initial props when iframe loads
const { postMessageData } = this.props;
this.sendMessage(postMessageData);
}

/**
*
* @param data
* @returns {string|*}
*/
serializePostMessageData(data) {
// Rely on the browser's built-in structured clone algorithm for serialization of the
// message as described in
// https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage
const { serializeMessage } = this.props;
if (!serializeMessage) {
return data;
}

// To be on the safe side we can also ignore the browser's built-in serialization feature
// and serialize the data manually.
if (typeof data === 'object') {
return JSON.stringify(data);
} if (typeof data === 'string') {
return data;
}
return `${data}`;
}

/**
*
* @param postMessageData
*/
sendMessage(postMessageData) {
// Using postMessage data from props will result in a subtle but deadly bug,
// where old data from props is being sent instead of new postMessageData.
// This is because data sent from componentWillReceiveProps is not yet
// in props but only in nextProps.
const { targetOrigin } = this.props;
const serializedData = this.serializePostMessageData(postMessageData);
this.frame.contentWindow.postMessage(serializedData, targetOrigin);
}

/**
*
* @returns {*}
*/
render() {
const { attributes } = this.props;
// define some sensible defaults for our iframe attributes
const defaultAttributes = {
allowFullScreen: false,
frameBorder: 0,
};
// then merge in the user's attributes with our defaults
const mergedAttributes = Object.assign(
{},
defaultAttributes,
attributes,
);
return (
<iframe //eslint-disable-line
ref={this.setIFrameElement}
{...mergedAttributes}
/>
);
}
}

IframeComm.defaultProps = {
attributes: {},
handleReady: {},
handleReceiveMessage: {},
serializeMessage: true,
targetOrigin: '*',
};

IframeComm.propTypes = {
/*
Iframe Attributes
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#Attributes
React Supported Attributes
https://facebook.github.io/react/docs/dom-elements.html#all-supported-html-attributes
Note: attributes are camelCase, not all lowercase as usually defined.
*/
attributes: PropTypes.shape({
allowFullScreen: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
frameBorder: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
name: PropTypes.string,
sandbox: PropTypes.string,
scrolling: PropTypes.string,
// https://www.html5rocks.com/en/tutorials/security/sandboxed-iframes/
src: PropTypes.string.isRequired,
srcDoc: PropTypes.string,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
/*
Callback function called when iframe loads. We're simply listening
to the iframe's `window.onload`. To ensure communication code in your iframe
is totally loaded, you can implement a syn-ack TCP-like handshake using
`postMessageData` and `handleReceiveMessage`.
*/
handleReady: PropTypes.func,
// Callback function called when iFrame sends the parent window a message.
handleReceiveMessage: PropTypes.func,

/*
You can pass it anything you want, we'll serialize to a string
preferablly use a simple string message or an object.
If you use an object, you need to follow the same naming convention
in the iframe so you can parse it accordingly.
*/
postMessageData: PropTypes.any.isRequired, //eslint-disable-line

/*
Enable use of the browser's built-in structured clone algorithm for serialization
by settings this to `false`.
Default is `true`, using our built in logic for serializing everything to a string.
*/
serializeMessage: PropTypes.bool,

/*
Always provide a specific targetOrigin, not *, if you know where the other window's
document should be located. Failing to provide a specific target discloses the data
you send to any interested malicious site.
*/
targetOrigin: PropTypes.string,
};
1 change: 1 addition & 0 deletions src/lib/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './IFrameComm';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new pattern that is needed?