PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / src / class-theme-styles-sync.php

class-theme-styles-sync.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at src/class-theme-styles-sync.php

222 lines 6.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The slice of the active theme's design that a post email can inherit.
4 *
5 * @package automattic/jetpack
6 */
7
8 namespace Automattic\Jetpack\Plugin;
9
10 use WP_Theme_JSON_Resolver;
11
12 /**
13 * Builds the `theme_styles` sync callable.
14 *
15 * Post emails render on WordPress.com against its own copy of a theme, so a site running a theme
16 * it does not ship inherits no design at all. See NL-943.
17 */
18 class Theme_Styles_Sync {
19
20 /**
21 * Refuse to sync a slice larger than this, in bytes.
22 *
23 * A bound on a pathological palette, not a budget: ordinary themes are orders below it, and a
24 * theme that trips it syncs nothing and keeps today's behaviour.
25 */
26 const MAX_PAYLOAD_BYTES = 51200;
27
28 /**
29 * The active theme's inheritable design.
30 *
31 * @return array|null Null when theme.json is unavailable, or the slice is oversized.
32 */
33 public static function get_theme_styles() {
34 if ( ! class_exists( 'WP_Theme_JSON_Resolver' ) ) {
35 return null;
36 }
37
38 $theme = WP_Theme_JSON_Resolver::get_theme_data();
39 $raw = $theme->get_raw_data();
40
41 // User customisations are deliberately absent: they already reach WordPress.com through the
42 // synced `wp_global_styles` post, which is layered over this.
43 $styles = self::inheritable_styles( $raw['styles'] ?? array() );
44
45 // Reported even when empty: sync skips a null value, which would leave the receiving end
46 // holding the previous theme's design with nothing to say the theme had changed.
47 $slice = array(
48 'stylesheet' => get_stylesheet(),
49 // Core's own, not a literal: it migrates raw data to the latest schema, so labelling a
50 // later shape with an older number would have the receiving end migrate it a second time.
51 'version' => $raw['version'] ?? 3,
52 'settings' => self::preset_sources( $raw['settings'] ?? array() ),
53 'styles' => $styles,
54 );
55
56 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.Missing -- measuring the wire representation, which Sync encodes with default flags.
57 $encoded = wp_json_encode( $slice );
58 if ( false === $encoded || strlen( $encoded ) > self::MAX_PAYLOAD_BYTES ) {
59 return null;
60 }
61
62 return $slice;
63 }
64
65 /**
66 * The preset definitions a style's `var(--wp--preset--…)` reference needs to be resolved.
67 *
68 * Styles travel unresolved on purpose. Resolving here would bake in the theme's stock value and
69 * lose the reference, so a creator who recolours a palette slug would keep getting the stock
70 * colour in their email while their site renders the new one. The receiving end resolves instead,
71 * against these merged under the creator's own record, which is the only place both are known.
72 *
73 * @param array $settings The theme's settings.
74 * @return array
75 */
76 private static function preset_sources( array $settings ) {
77 $sources = array( 'color' => array( 'palette' => self::flatten_presets( $settings['color']['palette'] ?? array() ) ) );
78
79 $wanted = array(
80 'typography' => array( 'fontSizes', 'fontFamilies' ),
81 'spacing' => array( 'spacingSizes' ),
82 );
83
84 foreach ( $wanted as $group => $keys ) {
85 foreach ( $keys as $key ) {
86 $presets = self::flatten_presets( $settings[ $group ][ $key ] ?? array() );
87 if ( ! empty( $presets ) ) {
88 $sources[ $group ][ $key ] = $presets;
89 }
90 }
91 }
92
93 // The font files are two thirds of a family's bytes and no use to a mail client, which cannot
94 // load a web font. Only the family name resolves a `var(--wp--preset--font-family--…)`.
95 if ( isset( $sources['typography']['fontFamilies'] ) ) {
96 $sources['typography']['fontFamilies'] = array_map( array( self::class, 'without_font_files' ), $sources['typography']['fontFamilies'] );
97 }
98
99 return $sources;
100 }
101
102 /**
103 * One font-family preset without its `fontFace` declarations.
104 *
105 * @param mixed $family A `fontFamilies` entry.
106 * @return mixed
107 */
108 private static function without_font_files( $family ) {
109 if ( is_array( $family ) ) {
110 unset( $family['fontFace'] );
111 }
112
113 return $family;
114 }
115
116 /**
117 * Keep only the style paths an email could act on.
118 *
119 * Deliberately a superset of the allowlist WordPress.com applies on receipt, so narrowing what
120 * an email inherits stays a WordPress.com-side change rather than one gated on a plugin release.
121 *
122 * @param array $styles The theme's styles.
123 * @return array
124 */
125 private static function inheritable_styles( array $styles ) {
126 $typography = array_fill_keys(
127 array( 'fontFamily', 'fontSize', 'fontWeight', 'fontStyle', 'lineHeight', 'letterSpacing', 'textDecoration', 'textTransform' ),
128 true
129 );
130 $color = array(
131 'text' => true,
132 'background' => true,
133 );
134
135 $element = array(
136 'typography' => $typography,
137 'color' => $color,
138 );
139 $elements = array(
140 'link' => $element,
141 'heading' => $element,
142 'button' => $element,
143 'caption' => $element,
144 );
145 foreach ( range( 1, 6 ) as $level ) {
146 $elements[ 'h' . $level ] = $element;
147 }
148
149 return self::intersect(
150 $styles,
151 array(
152 'color' => $color,
153 'typography' => $typography,
154 'spacing' => array(
155 'blockGap' => true,
156 'padding' => true,
157 'margin' => true,
158 ),
159 'elements' => $elements,
160 )
161 );
162 }
163
164 /**
165 * Keep only the branches an allowlist names, dropping empty ones entirely.
166 *
167 * @param array $styles A styles tree, or a branch of one.
168 * @param array $allowlist The matching branch of the allowlist.
169 * @return array
170 */
171 private static function intersect( array $styles, array $allowlist ) {
172 $kept = array();
173
174 foreach ( $allowlist as $key => $permitted ) {
175 if ( ! isset( $styles[ $key ] ) ) {
176 continue;
177 }
178
179 if ( true === $permitted ) {
180 $kept[ $key ] = $styles[ $key ];
181 continue;
182 }
183
184 if ( ! is_array( $styles[ $key ] ) ) {
185 continue;
186 }
187
188 $branch = self::intersect( $styles[ $key ], $permitted );
189 if ( ! empty( $branch ) ) {
190 $kept[ $key ] = $branch;
191 }
192 }
193
194 return $kept;
195 }
196
197 /**
198 * Flatten an origin-keyed preset list into the flat list `WP_Theme_JSON` expects for one origin.
199 *
200 * Each origin is checked rather than assumed: core's schema leaves a non-array preset list
201 * untouched and the `WP_Theme_JSON` constructor origin-keys it anyway, so a theme.json
202 * declaring a scalar there reaches this with a string where a list belongs.
203 *
204 * @param array $presets Preset list that may be origin-keyed or already flat.
205 * @return array
206 */
207 private static function flatten_presets( array $presets ) {
208 if ( empty( $presets ) || isset( $presets[0] ) ) {
209 return $presets;
210 }
211
212 $flat = array();
213 foreach ( array( 'default', 'blocks', 'theme', 'custom' ) as $origin ) {
214 if ( isset( $presets[ $origin ] ) && is_array( $presets[ $origin ] ) ) {
215 $flat = array_merge( $flat, $presets[ $origin ] );
216 }
217 }
218
219 return $flat;
220 }
221 }
222