@missing-elements/h5p-embed 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/package.json +3 -3
  3. package/site/embed.js +57 -25
package/README.md CHANGED
@@ -66,10 +66,14 @@ It works at a domain's root or under a path, over https. Serve `h5p-sw.js` with
66
66
  | `preload=auto` | Start fetching media at once |
67
67
  | `activity-id=<IRI>` | The xAPI object id, instead of the package URL |
68
68
  | `custom-css=<url>` | A stylesheet loaded into the content |
69
+ | `lang=<tag>` | The language of H5P's own buttons and dialogs, such as `de` or `pt-BR`. Default: English |
69
70
  | `xapi=<origin>` | Relay statements to the embedding page |
70
71
 
71
- A custom script and a learner's name cannot be set on the address. Opened unframed, the page asks
72
- before playing a package from another origin unless `--packages` names it.
72
+ A custom script and a learner's name cannot be set on the address. Opened unframed, the page plays
73
+ a package from another origin `untrusted` unless `--packages` names it: none of the package's own
74
+ code runs, its libraries come from this domain's pack or `--default-libraries` (never from the
75
+ address's `libraries=`), and a button offers to play it with its own scripts only when it needs
76
+ them.
73
77
 
74
78
  ## Messages
75
79
 
@@ -112,7 +116,7 @@ page's script, which reads the page's `<h5p-player>`, `#loader` and `#notice` (s
112
116
  `site/index.html` and `site/embed.css`). Its options: `librariesPack` (the URL `libraries=pack`
113
117
  names), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
114
118
  `@missing-elements/h5p-runtime` export, for a bundled element) and `askInOwnFrame` (default
115
- `true`; `false` skips the click when a same-origin page frames it).
119
+ `true`; `false` plays a package from another origin as it is when a same-origin page frames it).
116
120
 
117
121
  ```js
118
122
  import '@missing-elements/h5p-offline-player'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@missing-elements/h5p-embed",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Self-hosted H5P embeds: writes the H5P embed page as a static site for a player domain of your own, then any site frames it with an iframe and one script line. No H5P server, no third-party service; optional allowlists for package hosts and embedding sites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,8 +44,8 @@
44
44
  ],
45
45
  "dependencies": {
46
46
  "@missing-elements/h5p-libraries": "^0.1.1",
47
- "@missing-elements/h5p-offline-player": "^0.6.1",
48
- "@missing-elements/h5p-runtime": "^0.1.1"
47
+ "@missing-elements/h5p-offline-player": "^0.7.0",
48
+ "@missing-elements/h5p-runtime": "^0.1.2"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^26.6.4",
package/site/embed.js CHANGED
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * ?src=<package url>[&libraries=pack|<url> …|none][&preload=auto][&xapi=<parent origin>]
9
9
  * [&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
10
- * [&activity-id=<IRI>][&custom-css=<stylesheet url>]
10
+ * [&activity-id=<IRI>][&custom-css=<stylesheet url>][&lang=<language tag>]
11
11
  *
12
12
  * Upward it speaks H5P's own resizer protocol — the `hello` / `resize` exchange that h5p.org's
13
13
  * embed code and its `h5p-resizer.js` use — so a page that already resizes h5p.org iframes
@@ -26,15 +26,17 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
26
26
  * @param {string | null} [options.librariesPack] the URL of a copy of `@missing-elements/h5p-libraries`
27
27
  * on this site, which `libraries=pack` names; without one, `pack` is the element's copy on jsDelivr
28
28
  * @param {string[] | null} [options.packages] the origins packages and library bundles may come
29
- * from, besides this page's own; `null` plays any, asking first when the storage is this origin's
29
+ * from, besides this page's own; `null` plays any, one from another origin `untrusted` when the
30
+ * storage is this origin's
30
31
  * @param {string | null} [options.defaultLibraries] the `libraries` value for an address that has
31
32
  * none — `pack` or URLs, as the parameter — so a snippet without `&libraries=` still plays
32
33
  * an export that carries no libraries; `&libraries=none` turns it off for one address
33
34
  * @param {object | null} [options.runtime] the `runtime` export of `@missing-elements/h5p-runtime`,
34
35
  * for a page that bundles the element; without it the element looks for `frame-assets/` beside itself
35
- * @param {boolean} [options.askInOwnFrame] whether a package from another origin waits for a click
36
- * when this page is framed by a page of its own origin, whose storage it shares. A site whose
37
- * own preview frames the page for a package the visitor just chose passes `false`
36
+ * @param {boolean} [options.askInOwnFrame] whether a package from another origin plays `untrusted`,
37
+ * with a click to play it with its own code when it needs that, when this page is framed by a
38
+ * page of its own origin, whose storage it shares. A site whose own preview frames the page for
39
+ * a package the visitor just chose passes `false`
38
40
  */
39
41
  export function startEmbed({ librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
40
42
  const params = new URLSearchParams(location.search)
@@ -166,8 +168,16 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
166
168
 
167
169
  /* ---------------------------------------------------------------- errors, and Safari */
168
170
 
171
+ /** Asks whether to play an untrusted package with its own code; set when one is played so. */
172
+ let askToTrust = null
173
+
169
174
  player.addEventListener('error', (event) => {
170
- const { code, message } = event.detail
175
+ const { code, message, missingLibraries } = event.detail
176
+ // Played untrusted, it cannot do without its own copy of a library: a click decides.
177
+ if (missingLibraries?.carried && player.untrusted && askToTrust) {
178
+ askToTrust()
179
+ return
180
+ }
171
181
  if (code === 'no-worker' && framed) {
172
182
  // Detected, not sniffed: a browser, an in-app one or a page that is not https may give a
173
183
  // frame no Service Worker, and the player cannot run without one. Safari does allow it.
@@ -243,9 +253,10 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
243
253
  * The element's display options, by their attribute names, for the embedding page to ask for:
244
254
  * `&frame&copyright&export` shows H5P's action bar with those buttons, `&fullscreen=off` takes
245
255
  * that one away, `&activity-id=` names the statements' object and `&custom-css=` restyles the
246
- * content to the embedding site's taste. Not `custom-js`, `embed-code` or `user`: a script is
247
- * a capability on this origin that a link should not hand out, the embed is the embed, and a
248
- * learner's name has no place in a URL.
256
+ * content to the embedding site's taste. `&lang=` is the embedding page's language, which this
257
+ * page cannot read across origins: H5P's own buttons and dialogs follow it. Not `custom-js`,
258
+ * `embed-code` or `user`: a script is a capability on this origin that a link should not hand
259
+ * out, the embed is the embed, and a learner's name has no place in a URL.
249
260
  */
250
261
  const applyOptions = () => {
251
262
  for (const name of ['frame', 'copyright', 'export', 'icon', 'reporting']) {
@@ -256,6 +267,8 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
256
267
  const value = params.get(name)?.trim()
257
268
  if (value) player.setAttribute(name, value)
258
269
  }
270
+ const lang = params.get('lang')?.trim().replaceAll('_', '-')
271
+ if (lang && /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/.test(lang)) player.setAttribute('lang', lang)
259
272
  }
260
273
 
261
274
  const start = (value, libraries) => {
@@ -308,25 +321,44 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
308
321
  return
309
322
  }
310
323
 
311
- // A host on the list was vouched for when the site was set up; anything else from another
312
- // origin waits for a click when it would share this origin's storage.
324
+ // A host on the list was vouched for when the site was set up, and in another site's frame the
325
+ // storage is that site's own: those play as they are.
313
326
  if (origin === location.origin || allowed || !sharesOriginStorage()) {
314
327
  start(src, sources.value)
315
328
  return
316
329
  }
317
- loader.hidden = true
330
+
331
+ // Anything else from another origin would share this origin's storage. It plays untrusted, as
332
+ // data with the library pack's code and none of its own, and waits for a click that names the
333
+ // host only when it cannot play without its own, or there is no pack to play it with.
318
334
  const host = new URL(src, location.href).host || origin
319
- const button = document.createElement('button')
320
- button.type = 'button'
321
- button.textContent = 'Open the package'
322
- button.addEventListener('click', () => {
323
- say('')
324
- loader.hidden = false
325
- start(src, sources.value)
326
- })
327
- say(
328
- `This link opens a package from ${host}. A package runs its own scripts on this site, and they ` +
329
- 'can read what other packages saved in this browser. Open it only if you trust that site.'
330
- )
331
- notice.append(' ', button)
335
+ askToTrust = () => {
336
+ loader.hidden = true
337
+ const button = document.createElement('button')
338
+ button.type = 'button'
339
+ button.textContent = 'Open with its own scripts'
340
+ button.addEventListener('click', () => {
341
+ say('')
342
+ loader.hidden = false
343
+ player.untrusted = false
344
+ player.removeAttribute('src')
345
+ start(src, sources.value)
346
+ })
347
+ say(
348
+ `This package from ${host} plays only with its own scripts. They would run on this site and ` +
349
+ 'could read what other packages saved in this browser. Open it only if you trust that site.'
350
+ )
351
+ notice.append(' ', button)
352
+ }
353
+ // The address's `libraries=` is the link-writer's as much as its `src`, and a bundle's code runs
354
+ // as the host's own: an untrusted package plays with this site's sources alone, the page's
355
+ // default or its pack, and the address's only once the click has trusted it.
356
+ const own = defaultLibraries?.trim() ? librarySources(defaultLibraries.trim()).value : ''
357
+ const pack = own || librarySources('pack').value
358
+ if (!pack) {
359
+ askToTrust()
360
+ return
361
+ }
362
+ player.untrusted = true
363
+ start(src, pack)
332
364
  }