PPLR data maps: d3 is pinned, integrity-checked and kept off the host page's window
The critic fact-check (CRITIC-9 / CRITIC-5): the ROP Sunburst loaded d3@7 (a floating range, no SRI) as window.d3, overwriting a host's own d3, or running on whatever d3 the host already had. ensureD3() now loads d3@7.9.0 exactly with integrity + crossorigin, keeps it in the module (pplrD3()) and puts the host's window.d3 back (or leaves none). SemanticSunburst reads pplrD3(), and if a caller forgot to await ensureD3 it loads it and builds instead of asking for a refresh. Headless proof: the stubbed-DOM loader test, 6/6 (host d3 restored, none leaked, exact version + integrity + crossorigin, a failure rejects). BUNDLE=pplr-data-maps builds. Plugin-only. needs eyes: PPLR > ROP Sunburst draws; on a host page with its own window.d3 it is untouched.
This commit is contained in:
parent
e1b9db2265
commit
4c45d76c68
2 changed files with 28 additions and 7 deletions
|
|
@ -11,7 +11,7 @@
|
|||
* at least `LABEL_MIN_RADIANS` of arc, and font scales up at higher zoom
|
||||
* (label visibility re-evaluated on each zoom event).
|
||||
*
|
||||
* D3 is loaded from CDN globally — see index.html `<script src="d3.min.js">`.
|
||||
* D3 comes from ensureD3()/pplrD3() in pplrData.js: pinned, integrity-checked, never window.d3.
|
||||
*
|
||||
* Props:
|
||||
* tree — { name, code?, children?: Tree[], value?: number }
|
||||
|
|
@ -21,6 +21,7 @@
|
|||
* maxDepth — number — render up to this many rings (default 3)
|
||||
*/
|
||||
import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { pplrD3, ensureD3 } from '../utils/pplrData.js'
|
||||
|
||||
const props = defineProps({
|
||||
tree: { type: Object, required: true },
|
||||
|
|
@ -58,8 +59,9 @@ function arcColorOf(node) {
|
|||
const LABEL_MIN_RADIANS = 0.10
|
||||
|
||||
function build() {
|
||||
const d3 = window.d3
|
||||
if (!d3) { status.value = 'D3 not loaded — refresh page'; return }
|
||||
const d3 = pplrD3() // the pinned, scoped d3 (pplrData.js), never the host page's window.d3
|
||||
// not loaded yet (a caller that did not await ensureD3): load it, then build; no refresh needed
|
||||
if (!d3) { ensureD3().then(build, () => { status.value = 'D3 could not be loaded' }); return }
|
||||
const el = container.value
|
||||
if (!el || !props.tree) return
|
||||
|
||||
|
|
|
|||
|
|
@ -10,14 +10,33 @@ let base = './assets'
|
|||
export function setDataBase(b) { if (b) base = String(b).replace(/\/+$/, '') }
|
||||
export function dataUrl(file) { return base + '/pplr/' + file }
|
||||
|
||||
// d3 v7 is a page global here (as in the original build). Load it once, on first need.
|
||||
// d3 for the ROP Sunburst: PINNED, integrity-checked and kept OFF the host page's window
|
||||
// (CRITIC-9 / CRITIC-5, the critic fact-check). It used to load d3@7 (a floating range, no SRI) as
|
||||
// window.d3, overwriting a host page's own d3, or running on whatever d3 the host already had. Now one
|
||||
// exact version with integrity + crossorigin, captured here (pplrD3()), and the host's window.d3
|
||||
// put back exactly as it was.
|
||||
const D3_URL = 'https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js'
|
||||
const D3_SRI = 'sha384-CjloA8y00+1SDAUkjs099PVfnY2KmDC2BZnws9kh8D/lX1s46w6EPhpXdqMfjK6i'
|
||||
let d3Lib = null
|
||||
let d3Promise = null
|
||||
export function pplrD3() { return d3Lib }
|
||||
export function ensureD3() {
|
||||
if (typeof window === 'undefined' || window.d3) return Promise.resolve()
|
||||
if (d3Lib) return Promise.resolve(d3Lib)
|
||||
if (typeof window === 'undefined') return Promise.reject(new Error('no window'))
|
||||
if (!d3Promise) d3Promise = new Promise((res, rej) => {
|
||||
const hadOwn = Object.prototype.hasOwnProperty.call(window, 'd3')
|
||||
const hostD3 = window.d3
|
||||
const s = document.createElement('script')
|
||||
s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'
|
||||
s.onload = () => res(); s.onerror = () => { d3Promise = null; rej(new Error('d3 failed to load')) }
|
||||
s.src = D3_URL
|
||||
s.integrity = D3_SRI
|
||||
s.crossOrigin = 'anonymous'
|
||||
s.onload = () => {
|
||||
d3Lib = window.d3
|
||||
if (hadOwn) window.d3 = hostD3
|
||||
else { try { delete window.d3 } catch (_) { window.d3 = undefined } }
|
||||
res(d3Lib)
|
||||
}
|
||||
s.onerror = () => { d3Promise = null; rej(new Error('d3 failed to load (network or integrity)')) }
|
||||
document.head.appendChild(s)
|
||||
})
|
||||
return d3Promise
|
||||
|
|
|
|||
Loading…
Reference in a new issue