Skip to content

Commit

Permalink
Fix frame discoverability
Browse files Browse the repository at this point in the history
After one failed attempt to limit the frame discoverability (#3352), I
suggest here another temporary fix which involves:

* Disable the top-down, breadth-first traversal of frames to enable
  discoverability.

* Replace it by a targeted discoverability: identify the sender of the
  initial postMessage message and direct it to the right frame:

     - if the sender is the `sidebar` iframe (server frame), the
       postMessage is sent *only* to the `host` frame using
       `window.parent`.

     - if the sender is an annotatable iframe(s), send the postMessage
       to the `sidebar` iframe.

Pros:

- resolves the broken ePub example

- resolves the hyper-connectivity of frames

Cons:

- the `notebook` iframe is still not able to be discovered

- introduce an artificial delay on the discoverablity of annotatable
  iframes that could potentially brake

The client relies on an inter-frame communication system between the
`host` frame, where the client is initially loaded, and a number of
children iframes. For the communication to work, every frame needs to be
able to discover the iframe that acts as a server by sending a
`frame.postMessage`. Currently, the `sidebar` iframe is the server.

The following frames must establish communication with the server
`sidebar` iframe:

- `host` frame (where the client is initially loaded)
- `notebook` iframe
- additional annotatable iframe(s) (each have an `enable-annotation`
  attribute) where the another client instance is injected.

This layout represents the current arrangement of frames:

```
host frame (client)
|-> (generally, shadow DOMed) sidebar iframe (server)
|-> (generally, shadow DOMed) notebook iframe (client)
|-> [annotatable iframe/s] (client)
     |-> [annotatable iframe/s] (client)

```

There are two problems with the current discoverability algorithm:

1. It relies on `window.frames` to list all the other frames. Because
   `sidebar` and `notebook` iframes are generally wrapped on a shadow
   DOM they are not listed on `window.frames`.

2. It is very generic: the algorithm starts from the top-most frame in
   the hierarchy (`window.top`) and send messages to all the frame
   children *recursively*. If there are several clients initialised on
   individual frames, this algorithm causes *all* the `host` frames to
   be connected to all the `sidebar` iframes.
  • Loading branch information
esanzgar committed May 27, 2021
1 parent 7815342 commit 9e92a49
Showing 1 changed file with 70 additions and 18 deletions.
88 changes: 70 additions & 18 deletions src/shared/discovery.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ListenerCollection } from '../annotator/util/listener-collection';

/**
* Callback invoked when another frame is discovered in this window which runs
* the Hypothesis sidebar or annotation layer code.
Expand All @@ -8,7 +10,7 @@
* @param {string} token - A random identifier used by this frame.
*/

import { ListenerCollection } from '../annotator/util/listener-collection';
/** @typedef {import('../types/annotator').HypothesisWindow} HypothesisWindow */

/**
* Discovery finds frames in the current tab/window that can be annotated (the
Expand All @@ -20,9 +22,8 @@ import { ListenerCollection } from '../annotator/util/listener-collection';
*
* The discovery process works as follows:
*
* 1. Clients and servers perform a top-down, breadth-first traversal of the
* frame hierarchy in the tab and send either an "offer" (server) or
* "discovery" (client) message to each frame, except for their own frame.
* 1. Clients and servers perform a targeted search based on the iframe layout
* hierarchy.
* 2. Clients listen for "offer" messages and respond with "request" messages.
* 3. Servers listen for "discovery" messages and respond with "offer"
* messages.
Expand All @@ -34,7 +35,7 @@ import { ListenerCollection } from '../annotator/util/listener-collection';
*/
export default class Discovery {
/**
* @param {Window} target
* @param {HypothesisWindow} target
* @param {object} [options]
* @param {boolean} [options.server]
* @param {string} [options.origin]
Expand Down Expand Up @@ -99,26 +100,77 @@ export default class Discovery {
* Send a message to other frames in the current window to inform them about
* the existence of this frame and tell them whether this frame is a client
* or server.
*
* This layout describes the frame hierarchy:
*
* host frame (client)
* |-> (generally, shadow DOMed) sidebar iframe (server)
* |-> (generally, shadow DOMed) notebook iframe (client)
* |-> [annotatable iframe/s] (client)
* |-> [annotatable iframe/s] (client)
*
* Note: In the LMS context, the `host frame` is yet a descendant of a parent
* iframe (several levels up) to whom the `sidebar iframe` exchange configuration
* information. This communication is established in a different way, elsewhere.
*/
_beacon() {
let beaconMessage;
if (this.server) {
beaconMessage = '__cross_frame_dhcp_offer';
// Sidebar iframe
// Send the `offer` signal only to the `host frame`.
const hostFrame = this.target.parent;
hostFrame.postMessage('__cross_frame_dhcp_offer', this.origin);
} else {
beaconMessage = '__cross_frame_dhcp_discovery';
}
const beaconMessage = '__cross_frame_dhcp_discovery';

// Perform a top-down, breadth-first traversal of frames in the current
// window and send messages to them.
const queue = [this.target.top];
while (queue.length > 0) {
const parent = /** @type {Window} */ (queue.shift());
if (parent !== this.target) {
parent.postMessage(beaconMessage, this.origin);
// Annotatable iframe (can be nested)
if (this.target.__hypothesis_frame) {
// Find the host frame (which it is not an annotatable iframe)
let hostFrame;
do {
hostFrame = /** @type {HypothesisWindow} */ (this.target.parent);
} while (hostFrame.__hypothesis_frame);

// The sidebar iframe may not be yet created, therefore try to find the
// `sidebar iframe` at intervals
const findSidebar = (retries = 0) => {
// Sidebar iframe can be shadow DOMed
const shadowDomSidebar = /** @type {HTMLIFrameElement|null} */ (
hostFrame.document.querySelector('hypothesis-sidebar')
)?.shadowRoot?.querySelector('iframe')?.contentWindow;

// Or can be no shadow DOMed
const noShadowDomSidebar = /** @type {HTMLIFrameElement|null} */ (
hostFrame.document.querySelector(
'iframe[title="Hypothesis annotation viewer"]'
)
)?.contentWindow;

const sidebar = shadowDomSidebar ?? noShadowDomSidebar;

if (sidebar) {
setTimeout(
() => sidebar.postMessage(beaconMessage, this.origin),
1000 /* TODO: arbitrary delay as we can't check the readyState of the `sidebar iframe` because of cross-origins */
);
} else if (retries < 20) {
// Try for a max of 10s (20 * 500ms) in intervals of 500ms
setTimeout(() => findSidebar(retries + 1), 500);
}
};

findSidebar(0);
return;
}
for (let i = 0; i < parent.frames.length; i++) {
queue.push(parent.frames[i]);

// Notebook iframe
if (this.target.frameElement?.classList.contains('NotebookIframe')) {
// The sidebar iframe is unreachable, do nothing
return;
}

// Host frame
// Do not send a `discovery` signal, but wait for the sidebar to beacon
// the `offer` signal, instead.
}
}

Expand Down

0 comments on commit 9e92a49

Please sign in to comment.