22 * D3.js frontend renderer for Visualizer.
33 *
44 * Listens to the `visualizer:render:chart:start` event fired by render-facade.js.
5- * For charts with library === 'd3', retrieves the stored D3 code, converts the
6- * series/data arrays to plain objects, and executes the code via new Function.
5+ * For charts with library === 'd3', creates a sandboxed <iframe> and delegates
6+ * chart execution to it via postMessage — eliminating the persistent-XSS risk of
7+ * running stored code via new Function() in the main page context.
8+ *
9+ * The iframe uses srcdoc so no physical HTML file is needed. srcdoc iframes
10+ * always have a null origin regardless of sandbox, and sandbox="allow-scripts"
11+ * ensures the chart code cannot access the parent page's cookies, localStorage,
12+ * or DOM.
713 */
8- import * as d3 from 'd3' ;
9- import * as topojson from 'topojson-client' ;
10-
11- /** Convert Visualizer series + data arrays to plain objects for D3. */
12- function toD3Values ( series , data ) {
13- if ( ! Array . isArray ( series ) || ! Array . isArray ( data ) ) return [ ] ;
14- return data . map ( ( row ) => {
15- const obj = { } ;
16- series . forEach ( ( col , i ) => {
17- obj [ col . label ] = row [ i ] ;
18- } ) ;
19- return obj ;
20- } ) ;
14+
15+ function ensurePngName ( name ) {
16+ if ( ! name ) return 'chart.png' ;
17+ return name . toLowerCase ( ) . endsWith ( '.png' ) ? name : ` ${ name } .png` ;
18+ }
19+
20+ function downloadDataUrl ( dataUrl , name ) {
21+ const link = document . createElement ( 'a' ) ;
22+ link . href = dataUrl ;
23+ link . download = ensurePngName ( name ) ;
24+ document . body . appendChild ( link ) ;
25+ link . click ( ) ;
26+ link . remove ( ) ;
2127}
2228
2329/**
24- * Render a single D3 chart into its container element.
30+ * Render a single D3 chart into its container element via a sandboxed iframe .
2531 *
26- * @param {string } id - DOM element ID of the container
27- * @param {object } chart - chart entry from visualizer.charts
32+ * @param {string } id - DOM element ID of the container
33+ * @param {object } chart - chart entry from visualizer.charts
2834 */
2935function renderD3Chart ( id , chart ) {
3036 const container = document . getElementById ( id ) ;
@@ -37,97 +43,91 @@ function renderD3Chart( id, chart ) {
3743 return ;
3844 }
3945
40- const values = toD3Values ( chart . series , chart . data ) ;
46+ const iframeJsUrl =
47+ window . vizD3Renderer && window . vizD3Renderer . iframeJsUrl
48+ ? window . vizD3Renderer . iframeJsUrl
49+ : null ;
50+
51+ if ( ! iframeJsUrl ) {
52+ container . innerHTML =
53+ '<p style="color:#c00;padding:12px">D3 renderer iframe URL not configured.</p>' ;
54+ return ;
55+ }
4156
57+ // Match the iframe dimensions to the container after layout is complete.
4258 function doRender ( ) {
43- try {
44- // eslint-disable-next-line no-new-func
45- new Function ( 'd3' , 'topojson' , 'container' , 'data' , code ) ( d3 , topojson , container , values ) ;
46- } catch ( err ) {
47- container . innerHTML = '<p style="color:#c00;padding:12px">Chart render error: ' + err . message + '</p>' ;
59+ const width = container . offsetWidth || 800 ;
60+ const height = container . offsetHeight || 400 ;
61+
62+ // srcdoc iframes always have a null origin — no physical file needed.
63+ const srcdoc =
64+ '<!DOCTYPE html><html><head><meta charset="utf-8">' +
65+ '<style>*{margin:0;padding:0;box-sizing:border-box}body{overflow:hidden}' +
66+ '#chart{width:100%;height:100vh}</style></head>' +
67+ '<body><div id="chart"></div>' +
68+ '<script src="' + iframeJsUrl + '"><\/script></body></html>' ;
69+
70+ const iframe = document . createElement ( 'iframe' ) ;
71+ iframe . setAttribute ( 'sandbox' , 'allow-scripts' ) ;
72+ iframe . setAttribute ( 'srcdoc' , srcdoc ) ;
73+ iframe . setAttribute ( 'data-viz-id' , id ) ;
74+ iframe . style . cssText =
75+ 'border:0;width:' + width + 'px;height:' + height + 'px;display:block;' ;
76+
77+ // Once the iframe signals it is ready, send the render command.
78+ function onReady ( event ) {
79+ if ( event . source !== iframe . contentWindow ) return ;
80+ const msg = event . data ;
81+ if ( ! msg || msg . type !== 'iframe-ready' ) return ;
82+ window . removeEventListener ( 'message' , onReady ) ;
83+ iframe . contentWindow . postMessage (
84+ { type : 'render' , code, series : chart . series , data : chart . data } ,
85+ '*'
86+ ) ;
4887 }
88+
89+ window . addEventListener ( 'message' , onReady ) ;
90+
91+ container . innerHTML = '' ;
92+ container . appendChild ( iframe ) ;
4993 }
5094
5195 // Double requestAnimationFrame — ensures browser has completed layout before measuring.
5296 requestAnimationFrame ( ( ) => requestAnimationFrame ( doRender ) ) ;
5397}
5498
55- function ensurePngName ( name ) {
56- if ( ! name ) return 'chart.png' ;
57- return name . toLowerCase ( ) . endsWith ( '.png' ) ? name : `${ name } .png` ;
58- }
59-
60- function downloadDataUrl ( dataUrl , name ) {
61- const link = document . createElement ( 'a' ) ;
62- link . href = dataUrl ;
63- link . download = ensurePngName ( name ) ;
64- document . body . appendChild ( link ) ;
65- link . click ( ) ;
66- link . remove ( ) ;
67- }
68-
69- function svgToPng ( svg , callback ) {
70- const rect = svg . getBoundingClientRect ( ) ;
71- const width = parseFloat ( svg . getAttribute ( 'width' ) ) || rect . width || 800 ;
72- const height = parseFloat ( svg . getAttribute ( 'height' ) ) || rect . height || 600 ;
73- const clone = svg . cloneNode ( true ) ;
74- clone . setAttribute ( 'width' , width ) ;
75- clone . setAttribute ( 'height' , height ) ;
76- const serializer = new XMLSerializer ( ) ;
77- const svgText = serializer . serializeToString ( clone ) ;
78- const svgDataUrl = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent ( svgText ) ;
79-
80- const img = new Image ( ) ;
81- img . onload = ( ) => {
82- const canvas = document . createElement ( 'canvas' ) ;
83- canvas . width = width ;
84- canvas . height = height ;
85- const ctx = canvas . getContext ( '2d' ) ;
86- ctx . fillStyle = '#ffffff' ;
87- ctx . fillRect ( 0 , 0 , width , height ) ;
88- ctx . drawImage ( img , 0 , 0 ) ;
89- callback ( canvas . toDataURL ( 'image/png' ) ) ;
90- } ;
91- img . onerror = ( ) => callback ( null ) ;
92- img . src = svgDataUrl ;
93- }
94-
9599function handleImageAction ( id , name , action ) {
96100 const container = document . getElementById ( id ) ;
97101 if ( ! container ) return ;
98102
99- const canvas = container . querySelector ( 'canvas' ) ;
100- if ( canvas && typeof canvas . toDataURL === 'function' ) {
101- const img = canvas . toDataURL ( 'image/png' ) ;
102- if ( action === 'print' ) {
103- const win = window . open ( ) ;
104- win . document . write ( "<br><img src='" + img + "'/>" ) ;
105- win . document . close ( ) ;
106- win . onload = function ( ) { win . print ( ) ; setTimeout ( win . close , 500 ) ; } ;
107- } else {
108- downloadDataUrl ( img , name ) ;
109- }
110- return ;
111- }
103+ const iframe = container . querySelector ( 'iframe[data-viz-id]' ) ;
104+ if ( ! iframe || ! iframe . contentWindow ) return ;
105+
106+ function onResult ( event ) {
107+ if ( event . source !== iframe . contentWindow ) return ;
108+ const msg = event . data ;
109+ if ( ! msg || msg . type !== 'export-image-result' ) return ;
110+ window . removeEventListener ( 'message' , onResult ) ;
112111
113- const svg = container . querySelector ( 'svg' ) ;
114- if ( ! svg ) return ;
112+ const dataUrl = msg . dataUrl ;
113+ if ( ! dataUrl ) return ;
115114
116- svgToPng ( svg , ( img ) => {
117- if ( ! img ) return ;
118115 if ( action === 'print' ) {
119116 const win = window . open ( ) ;
120- win . document . write ( "<br><img src='" + img + "'/>" ) ;
117+ win . document . write ( "<br><img src='" + dataUrl + "'/>" ) ;
121118 win . document . close ( ) ;
122119 win . onload = function ( ) { win . print ( ) ; setTimeout ( win . close , 500 ) ; } ;
123120 } else {
124- downloadDataUrl ( img , name ) ;
121+ downloadDataUrl ( dataUrl , name ) ;
125122 }
126- } ) ;
123+ }
124+
125+ window . addEventListener ( 'message' , onResult ) ;
126+ iframe . contentWindow . postMessage ( { type : 'export-image' } , '*' ) ;
127127}
128128
129129( function ( $ ) {
130- $ ( 'body' ) . on ( 'visualizer:render:chart:start' , function ( e , viz ) {
130+ $ ( 'body' ) . on ( 'visualizer:render:chart:start' , function ( _e , viz ) {
131131 if ( ! viz . charts ) return ;
132132
133133 // Frontend mode: a specific chart ID is provided.
@@ -148,7 +148,7 @@ function handleImageAction( id, name, action ) {
148148 } ) ;
149149 } ) ;
150150
151- $ ( 'body' ) . on ( 'visualizer:action:specificchart' , function ( event , v ) {
151+ $ ( 'body' ) . on ( 'visualizer:action:specificchart' , function ( _event , v ) {
152152 if ( v . action !== 'image' && v . action !== 'print' ) return ;
153153 handleImageAction ( v . id , v ?. dataObj ?. name , v . action ) ;
154154 } ) ;
0 commit comments