PluginProbe
M Chart / 2.3.1
M Chart v2.3.1
2.3.2 2.3.1 2.3 2.2.2 2.2.1 2.2 trunk 1.0 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.10 1.10.1 1.11 1.11.1 1.11.2 1.12 1.2 1.2.1 1.3 1.3.1 1.3.2 All 53 releases
m-chart / components / templates / iframe.php

iframe.php in M Chart 2.3.1, at components/templates/iframe.php

182 lines 7.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5
6 // Make sure we instantiate the library so any library specific filters/setup get run
7 $library = $this->get_post_meta( $post->ID, 'library' );
8 $this->library( $library );
9
10 // Resolve all CSP allowlists and font descriptors upfront so we can emit one header before any output begins
11 $style_urls = apply_filters( 'm_chart_iframe_styles', [], $post->ID );
12 $inline_styles = apply_filters( 'm_chart_iframe_inline_styles', [], $post->ID );
13 $font_descriptors = apply_filters( 'm_chart_iframe_fonts', [], $post->ID );
14
15 // Default allowed style-src hosts cover the well-known font-service stylesheet origins
16 // Site owners using Fontspring, FontAwesome CDN, an internal CDN, etc. can extend via the filter
17 $style_hosts = apply_filters( 'm_chart_iframe_csp_style_src', [ 'fonts.googleapis.com', 'use.typekit.net' ], $post->ID );
18 // Default allowed font-src hosts cover where Google Fonts + Adobe Typekit actually load their .woff2 files from
19 $font_hosts = apply_filters( 'm_chart_iframe_csp_font_src', [ 'fonts.gstatic.com', 'use.typekit.net', 'p.typekit.net' ], $post->ID );
20
21 // Auto-extract hosts from anything passed through m_chart_iframe_styles
22 // Site owners can add a stylesheet URL without also having to register its host in the CSP filter
23 foreach ( $style_urls as $style_url ) {
24 $host = wp_parse_url( $style_url, PHP_URL_HOST );
25
26 if ( $host ) {
27 $style_hosts[] = $host;
28 }
29 }
30
31 // Auto-extract hosts from m_chart_iframe_fonts font sources too
32 foreach ( $font_descriptors as $font ) {
33 foreach ( (array) ( $font['src'] ?? [] ) as $src ) {
34 $host = wp_parse_url( $src['url'] ?? '', PHP_URL_HOST );
35
36 if ( $host ) {
37 $font_hosts[] = $host;
38 }
39 }
40 }
41
42 $style_hosts = array_unique( array_filter( $style_hosts ) );
43 $font_hosts = array_unique( array_filter( $font_hosts ) );
44
45 // frame-ancestors controls who's allowed to embed this iframe page
46 // Default 'self' permits same-origin embedding; site owners syndicating charts to third-party sites can add origins here
47 $frame_ancestors = apply_filters( 'm_chart_iframe_frame_ancestors', [ "'self'" ], $post->ID );
48
49 // Generate per-response CSP nonce — cryptographically random, NOT a CSRF nonce (wp_create_nonce is the wrong tool here)
50 $nonce = bin2hex( random_bytes( 16 ) );
51
52 // Whitelist the characters that can appear in a valid CSP source expression
53 // esc_attr() is for HTML attributes — it would encode 'self' as &#039;self&#039; here and break the directive
54 // Stripping anything outside this character class prevents header injection (CRLF, semicolons) while preserving keywords/hosts/schemes
55 $sanitize_csp_source = static function ( $value ) {
56 return preg_replace( '/[^A-Za-z0-9.\-_:\/*\'+=@]/', '', (string) $value );
57 };
58
59 // style-src uses 'unsafe-inline' (not a nonce) because inline style="..." attributes can't carry a nonce
60 // CSS-driven exfiltration vectors (url(), @font-face src, @import) are still blocked by font-src / img-src / connect-src
61 // script-src keeps a nonce since the chart template emits a dynamic inline <script> we want strictly gated
62 $csp = sprintf(
63 "default-src 'none'; style-src 'self' 'unsafe-inline' %s; script-src 'self' 'nonce-%s'; font-src 'self' data: %s; img-src 'self' data:; connect-src 'self'; frame-ancestors %s;",
64 implode( ' ', array_map( $sanitize_csp_source, $style_hosts ) ),
65 $nonce,
66 implode( ' ', array_map( $sanitize_csp_source, $font_hosts ) ),
67 implode( ' ', array_map( $sanitize_csp_source, $frame_ancestors ) )
68 );
69
70 header( 'Content-Security-Policy: ' . $csp );
71
72 // Expose the nonce to chartjs-chart.php so its inline <script> can carry a matching nonce attribute
73 $this->iframe_csp_nonce = $nonce;
74 ?>
75 <!doctype html>
76 <html <?php language_attributes(); ?>>
77 <head>
78 <meta charset="utf-8">
79 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
80 <title><?php echo esc_html( get_the_title( $post->ID ) ); ?></title>
81 <meta name="description" content="">
82 <meta name="viewport" content="width=device-width, initial-scale=1">
83 <?php wp_print_scripts( apply_filters( 'm_chart_iframe_scripts', $scripts, $post->ID ) ); ?>
84 <?php
85 // Stylesheet URLs to inject into the iframe head
86 // Used by extensions that need to load external CSS (e.g. Google Fonts, Adobe Typekit) inside the iframe
87 // iframes don't inherit parent-page font/style loads
88 foreach ( $style_urls as $style_url ) {
89 printf( '<link rel="stylesheet" href="%s" />' . "\n", esc_url( $style_url ) );
90 }
91
92 // Inline CSS strings to inject into the iframe head
93 // Used by extensions that need to emit raw CSS — typically custom-uploaded @font-face rules where the producer's own sanitizer is the trust boundary
94 // Distinct from m_chart_iframe_fonts below, which generates @font-face from validated field-by-field descriptors
95 // Use this hook only when the CSS is already authored as CSS (e.g. m-chart-pro themes' customFontFace blocks)
96 foreach ( $inline_styles as $css ) {
97 if ( '' === trim( (string) $css ) ) {
98 continue;
99 }
100
101 printf( "<style>%s</style>\n", $css );
102 }
103
104 // Structured @font-face injection — render each descriptor through a known-safe template
105 // Every slot is allowlisted: family is char-class-stripped, weight/style/display are enum-validated
106 // Src URLs are esc_url'd with HTTPS-only scheme constraint, format is alnum-stripped
107 foreach ( $font_descriptors as $font ) {
108 $family = preg_replace( '/[^A-Za-z0-9 _-]/', '', (string) ( $font['family'] ?? '' ) );
109
110 if ( '' === $family ) {
111 continue;
112 }
113
114 $weight = preg_match( '/^[1-9]00$|^(normal|bold)$/', (string) ( $font['weight'] ?? '400' ) ) ? $font['weight'] : '400';
115 $style = in_array( $font['style'] ?? 'normal', [ 'normal', 'italic', 'oblique' ], true ) ? $font['style'] : 'normal';
116 $display = in_array( $font['display'] ?? 'swap', [ 'auto', 'block', 'swap', 'fallback', 'optional' ], true ) ? $font['display'] : 'swap';
117
118 $srcs = [];
119
120 foreach ( (array) ( $font['src'] ?? [] ) as $src ) {
121 $url = esc_url( $src['url'] ?? '', [ 'https' ] );
122
123 if ( '' === $url ) {
124 continue;
125 }
126
127 $format = preg_replace( '/[^a-z0-9-]/', '', strtolower( (string) ( $src['format'] ?? '' ) ) );
128 $srcs[] = sprintf( "url('%s')%s", $url, $format ? " format('{$format}')" : '' );
129 }
130
131 if ( empty( $srcs ) ) {
132 continue;
133 }
134
135 printf(
136 "<style>@font-face { font-family: '%s'; src: %s; font-weight: %s; font-style: %s; font-display: %s; }</style>\n",
137 esc_html( $family ),
138 implode( ', ', $srcs ),
139 esc_html( $weight ),
140 esc_html( $style ),
141 esc_html( $display )
142 );
143 }
144 ?>
145 <style>
146 /* This prevents the screenreader stuff from being visible just like it behaves in it's regular context in the site */
147 .screen-reader-text {
148 border: 0;
149 clip-path: inset(50%);
150 height: 1px;
151 margin: -1px;
152 overflow: hidden;
153 padding: 0;
154 position: absolute;
155 width: 1px;
156 word-wrap: normal !important;
157 }
158
159 /* Un-hide the source attribution while its link has keyboard focus, mirrors m-chart-frontend.scss */
160 .m-chart-source.screen-reader-text:focus-within {
161 position: static;
162 width: auto;
163 height: auto;
164 margin: 0;
165 overflow: visible;
166 clip-path: none;
167 }
168
169 /* Strip user-agent margin from <figure> so the chart fills the iframe edge-to-edge */
170 body, figure { margin: 0; }
171 </style>
172 </head>
173 <!-- overflow: hidden; prevents the iframe from scrolling -->
174 <body style="overflow: hidden;">
175 <?php
176 $args = array_intersect_key( $_GET, $this->get_chart_default_args );
177 $args = array_map( 'sanitize_text_field', $args );
178 echo $this->get_chart( $post->ID, $args );
179 ?>
180 </body>
181 </html>
182