hypothesis / client

The Hypothesis web-based annotation client.
Other
630 stars 196 forks source link

Hypothesis expects Host in parent frame, not in any ancestor or top frame #6474

Open jwgmeligmeyling opened 1 month ago

jwgmeligmeyling commented 1 month ago

As hosts cannot inject the Hypothesis Client into a cross origin frame, we experimented loading the Hypothesis client into the frame through an intermediary subframe. This leads to the following structure:

The intermediate frame has subFrameIdentifier set. This way we do not load the sidebar, notebook and profile in this frame. Injection to the child frames works fine.

However, the child frame posts its messages directly to the parent window, while most of Hypothesis client runs in the top frame. As a quick fix we temporarily set the hostFrame to window.top and that seemed to do the trick:

diff --git a/src/annotator/index.ts b/src/annotator/index.ts
index e6ed85b0c..116384924 100644
--- a/src/annotator/index.ts
+++ b/src/annotator/index.ts
@@ -80,7 +80,7 @@ function init() {
   });
   sidebarLinkElement.addEventListener('destroy', resolveUnloadRequested);

-  const hostFrame = annotatorConfig.subFrameIdentifier ? window.parent : window;
+  const hostFrame = annotatorConfig.subFrameIdentifier ? window.top! : window;

   const destroyables = [] as Destroyable[];

However, it appears to be incorrect to assume the Hypothesis Client will always be loaded in the top frame. From the documentation it appears that for example the VitalSourceInjector assumes a setup where the Book container frame (not the top frame) appears to be the host:

/**
 * VitalSourceInjector runs in the book container frame and loads the client into
 * book content frames.
 *
 * The frame structure of the VitalSource book reader looks like this:
 *
 * [VitalSource top frame - bookshelf.vitalsource.com]
 *   |
 *   [Book container frame - jigsaw.vitalsource.com]
 *     |
 *     [Book content frame - jigsaw.vitalsource.com]
 *
 * The Hypothesis client can be initially loaded in the container frame or the
 * content frame. As the user navigates around the book, the container frame
 * remains the same but the content frame is swapped out. When used in the
 * container frame, this class handles initial injection of the client as a
 * guest in the current content frame, and re-injecting the client into new
 * content frames when they are created.
 */

Not sure though if I am interpreting this part of the documentation correctly.

However, perhaps we can find the correct Hypothesis frame using an ancestorLevel configuration property, analog to the same property for requestConfigFromFrame.

  1. Am I missing any existing means to correctly have a grand-child frame interact with the top frame?
  2. Is there interest in supporting this use case?
  3. If so, I am willing to contribute a PR, but what should an implementation look like? Does the ancestorLevel approach seem sensible?
robertknight commented 1 month ago

Hello, sorry for the delay in responding.

Top frame - mywebsite.com (host) Intermediate frame - myviewer.com (injects in to child) Child frame - myviewer.com (posts messages to host with injected client)

Is there a reason that the myviewer.com child frame cannot load the Hypothesis client as a guest directly?

However, it appears to be incorrect to assume the Hypothesis Client will always be loaded in the top frame. From the documentation it appears that for example the VitalSourceInjector assumes a setup where the Book container frame (not the top frame) appears to be the host:

Indeed, we have important use cases where the host frame is not the top frame.

Am I missing any existing means to correctly have a grand-child frame interact with the top frame?

You are not missing anything. We introduced the constraint that guests have to be direct children of hosts a while back as a simplification for the use cases we cared about at the time.

The simplest structure here would be if the myviewer.com child could load the Hypothesis client directly as a guest. If that is not possible, then a configuration option specifying how many levels up the tree the host is, like ancestorLevel, is I think the right way to go.

A possible structure for the configuration would be something like:

hostFrame: {
  origin: 'mywebsite.com',
  ancestorLevel: 2,
}

Where origin could be optional and default to * (any origin, as in window.postMessage).

jwgmeligmeyling commented 1 month ago

Hi @robertknight , thanks for getting back at me!

Is there a reason that the myviewer.com child frame cannot load the Hypothesis client as a guest directly?

The reason primarily being that the child frame content is from a CDN serving Pdf2HtmlEx output (for which I am developing an extension in our fork https://github.com/athenagroup/hypothesis-client/pull/1). So without the frame in between I would need to proxy the requests and inject the script through that means. (I only found out about viahtml after already cooking up the necessary changes 😅 ). I cannot serve the viewer from the host level domain as I do not control the host (in this case an LMS).

A possible structure for the configuration would be something like:

I'll have a look if we can implement this solution in a PR.