PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.3
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.3
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-wp-build-polyfills / src / class-wp-build-polyfills.php
jetpack / jetpack_vendor / automattic / jetpack-wp-build-polyfills / src Last commit date
internal 3 months ago js 3 weeks ago class-wp-build-polyfills.php 3 weeks ago
class-wp-build-polyfills.php
344 lines
1 <?php
2 /**
3 * Polyfill registration for Core packages not available or incomplete in older WordPress versions.
4 *
5 * Conditionally registers wp-notices, wp-private-apis, wp-theme (classic scripts) and
6 * `@wordpress/boot`, `@wordpress/route`, `@wordpress/a11y` (script modules)
7 * ONLY when they are not already provided by Core or Gutenberg.
8 *
9 * @package automattic/jetpack-wp-build-polyfills
10 */
11
12 namespace Automattic\Jetpack\WP_Build_Polyfills;
13
14 /**
15 * Registers polyfill scripts and modules for WordPress Core packages.
16 */
17 class WP_Build_Polyfills {
18
19 /**
20 * Available polyfill handles for classic scripts.
21 */
22 const SCRIPT_HANDLES = array( 'wp-notices', 'wp-private-apis', 'wp-rich-text', 'wp-theme', 'wp-views' );
23
24 /**
25 * Available polyfill module IDs.
26 */
27 const MODULE_IDS = array( '@wordpress/boot', '@wordpress/route', '@wordpress/a11y' );
28
29 /**
30 * Polyfills that only work when another polyfill is registered alongside them.
31 *
32 * These bundles call `__dangerousOptInToUnstableAPIsOnlyForCoreModules()` at
33 * module scope, which throws unless the `wp-private-apis` implementation that
34 * actually loads allowlists their package name. Core's allowlist does not:
35 * WP 6.9 omits `@wordpress/rich-text`, `@wordpress/theme` and
36 * `@wordpress/views`, and WP 7.0 still omits `@wordpress/compose` (bundled
37 * into the rich-text polyfill). Requesting one of these without
38 * `wp-private-apis` therefore throws at load time and blanks the page.
39 *
40 * The `wp-private-apis` script dependency in each `.asset.php` is not enough
41 * on its own — it makes WordPress enqueue the *handle*, which resolves to
42 * Core's incomplete implementation unless the polyfill was requested too.
43 *
44 * @var array<string, string[]>
45 */
46 const SCRIPT_DEPENDENCIES = array(
47 'wp-rich-text' => array( 'wp-private-apis' ),
48 'wp-theme' => array( 'wp-private-apis' ),
49 'wp-views' => array( 'wp-private-apis' ),
50 );
51
52 /**
53 * Minimum Gutenberg plugin version known to ship a private-apis allowlist
54 * that includes the dashboard packages used by this package's current build.
55 */
56 const GUTENBERG_PRIVATE_APIS_MIN_VERSION = '23.5.0';
57
58 /**
59 * Minimum Gutenberg plugin version whose rich-text ships all the privateApis
60 * keys dashboard packages unlock (useRichText, KeyboardShortcutContext,
61 * InputEventContext, shortcutsListener, inputEventsListener). They were
62 * completed by Gutenberg PR #78471, first released in 23.6.0 — verified
63 * against the released builds: 23.5.0 lacks three of the five keys.
64 */
65 const GUTENBERG_RICH_TEXT_MIN_VERSION = '23.6.0';
66
67 /**
68 * Tracks which polyfills have been requested and by which consumers.
69 *
70 * Keys are polyfill handles/module IDs, values are arrays of consumer names.
71 *
72 * @var array<string, string[]>
73 */
74 private static $requested = array();
75
76 /**
77 * Whether the wp_default_scripts hook has already been added.
78 *
79 * @var bool
80 */
81 private static $hooked = false;
82
83 /**
84 * The WordPress version below which force-replacements are applied.
85 * When multiple consumers call register() with different thresholds,
86 * the highest threshold wins (most conservative approach).
87 *
88 * @var string
89 */
90 private static $wp_version_threshold = '7.0';
91
92 /**
93 * Register polyfill scripts and modules.
94 *
95 * Call this early (e.g. during plugin load) — it hooks into wp_default_scripts
96 * at priority 20 so Core (default) and Gutenberg (priority 10) register first.
97 *
98 * When multiple consumers call this method with different thresholds, the
99 * highest threshold wins (most conservative — polyfills active on more versions).
100 *
101 * Polyfills listed in SCRIPT_DEPENDENCIES pull in their companion polyfill
102 * automatically, so consumers cannot request a combination that throws at
103 * load time. Those companions show up in get_consumers() under the
104 * requesting consumer's name.
105 *
106 * @param string $consumer A unique identifier for the consumer (e.g. plugin slug).
107 * @param string[] $polyfills List of polyfill handles/module IDs to register.
108 * Use class constants SCRIPT_HANDLES and MODULE_IDS for reference.
109 * @param string $wp_version_threshold The WordPress version below which force-replacements
110 * are applied. Defaults to '7.0'.
111 */
112 public static function register( $consumer, $polyfills, $wp_version_threshold = '7.0' ) {
113 foreach ( $polyfills as $handle ) {
114 if ( ! in_array( $handle, self::SCRIPT_HANDLES, true ) && ! in_array( $handle, self::MODULE_IDS, true ) ) {
115 continue;
116 }
117
118 $required = array_merge( array( $handle ), self::SCRIPT_DEPENDENCIES[ $handle ] ?? array() );
119
120 foreach ( $required as $required_handle ) {
121 if ( ! isset( self::$requested[ $required_handle ] ) ) {
122 self::$requested[ $required_handle ] = array();
123 }
124 if ( ! in_array( $consumer, self::$requested[ $required_handle ], true ) ) {
125 self::$requested[ $required_handle ][] = $consumer;
126 }
127 }
128 }
129
130 if ( version_compare( $wp_version_threshold, self::$wp_version_threshold, '>' ) ) {
131 self::$wp_version_threshold = $wp_version_threshold;
132 }
133
134 if ( self::$hooked ) {
135 return;
136 }
137 self::$hooked = true;
138
139 $package_root = dirname( __DIR__ );
140 $build_dir = $package_root . '/build';
141 $base_file = $package_root . '/composer.json';
142
143 // `wp_default_scripts` fires once when the WP_Scripts singleton is
144 // instantiated. If something has already initialized `wp_scripts()` —
145 // common on admin requests where WP or other plugins register scripts
146 // before `admin_menu` priority 1 runs — adding this hook here is too
147 // late and the polyfills never register. Detect that case and run the
148 // registration synchronously so consumers can rely on the script
149 // handles and module IDs being available regardless of init order.
150 if ( did_action( 'wp_default_scripts' ) ) {
151 self::register_scripts( wp_scripts(), $build_dir, $base_file, self::$wp_version_threshold );
152 self::register_modules( $build_dir, $base_file );
153 return;
154 }
155
156 add_action(
157 'wp_default_scripts',
158 function ( $scripts ) use ( $build_dir, $base_file ) {
159 self::register_scripts( $scripts, $build_dir, $base_file, self::$wp_version_threshold );
160 self::register_modules( $build_dir, $base_file );
161 },
162 20
163 );
164 }
165
166 /**
167 * Get the map of requested polyfills and their consumers.
168 *
169 * @return array<string, string[]> Keys are polyfill handles/module IDs, values are consumer names.
170 */
171 public static function get_consumers() {
172 return self::$requested;
173 }
174
175 /**
176 * Register polyfill classic scripts.
177 *
178 * @param \WP_Scripts $scripts The WP_Scripts instance.
179 * @param string $build_dir Absolute path to the build directory.
180 * @param string $base_file File path for plugins_url() computation.
181 * @param string $wp_version_threshold WP version below which force-replacements apply.
182 */
183 private static function register_scripts( $scripts, $build_dir, $base_file, $wp_version_threshold ) {
184 // Force-replace only when Core's bundled scripts are incomplete and
185 // Gutenberg cannot be trusted to provide a compatible implementation.
186 $gutenberg_version = defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null;
187
188 $polyfills = array(
189 'wp-notices' => array(
190 'path' => 'notices',
191 'force_threshold' => '7.0',
192 // Only force-replace on older WP without Gutenberg: older Core
193 // versions ship notices without SnackbarNotices and InlineNotices
194 // component exports that @wordpress/boot depends on.
195 ),
196 'wp-private-apis' => array(
197 'path' => 'private-apis',
198 'force_threshold' => '7.1',
199 'gutenberg_min_version' => self::GUTENBERG_PRIVATE_APIS_MIN_VERSION,
200 // WP 7.0 and older versions ship private-apis with an incomplete
201 // allowlist that rejects @wordpress/theme, @wordpress/route, and
202 // newer dashboard packages. Active Gutenberg is only a safe
203 // substitute once its private-apis allowlist includes those
204 // dashboard packages too.
205 ),
206 'wp-rich-text' => array(
207 'path' => 'rich-text',
208 'force_threshold' => '7.1',
209 'gutenberg_min_version' => self::GUTENBERG_RICH_TEXT_MIN_VERSION,
210 // WP 7.0 and older ship a rich-text whose `privateApis` current
211 // dashboard dependencies cannot use (e.g. @wordpress/dataviews
212 // >= 17.2 dataform controls, which unlock it at module scope).
213 // WP 6.9 exports no `privateApis` at all, which throws "Cannot
214 // unlock an undefined object"; WP 7.0 exports one locked with
215 // only `useRichText`, so destructuring the other keys yields
216 // undefined. Either way the page blanks. Older Gutenberg is not
217 // a safe substitute either — see the constant's doc.
218 ),
219 'wp-theme' => array(
220 'path' => 'theme',
221 ),
222 'wp-views' => array(
223 'path' => 'views',
224 ),
225 );
226
227 foreach ( $polyfills as $handle => $data ) {
228 if ( ! isset( self::$requested[ $handle ] ) ) {
229 continue;
230 }
231
232 $asset_file = $build_dir . '/scripts/' . $data['path'] . '/index.asset.php';
233
234 if ( ! file_exists( $asset_file ) ) {
235 continue;
236 }
237
238 $force_threshold = $data['force_threshold'] ?? null;
239 if ( null !== $force_threshold && version_compare( $wp_version_threshold, $force_threshold, '>' ) ) {
240 $force_threshold = $wp_version_threshold;
241 }
242
243 $force = null !== $force_threshold
244 && ! self::is_gutenberg_version_safe( $data['gutenberg_min_version'] ?? null, $gutenberg_version )
245 && version_compare( $GLOBALS['wp_version'] ?? '0', $force_threshold, '<' );
246
247 if ( ! $force && $scripts->query( $handle, 'registered' ) ) {
248 continue;
249 }
250
251 // Deregister first when forcing replacement of an existing registration.
252 // `remove()` drops everything Core set up alongside the src — notably
253 // `$args` (Core registers package scripts with `1`, i.e. in the footer)
254 // and the registered translations. Both are restored after `add()` so
255 // the replacement is a drop-in for the registration it displaces.
256 $replaced = null;
257 if ( $force && $scripts->query( $handle, 'registered' ) ) {
258 $replaced = $scripts->registered[ $handle ];
259 $scripts->remove( $handle );
260 }
261
262 $asset = require $asset_file;
263
264 $scripts->add(
265 $handle,
266 plugins_url( 'build/scripts/' . $data['path'] . '/index.js', $base_file ),
267 $asset['dependencies'],
268 $asset['version'],
269 // Match Core's `wp_default_packages_scripts()`, which registers every
270 // `wp-*` package script in the footer.
271 null !== $replaced ? $replaced->args : 1
272 );
273
274 if ( null !== $replaced && null !== $replaced->textdomain ) {
275 $scripts->set_translations( $handle, $replaced->textdomain, $replaced->translations_path );
276 } elseif ( in_array( 'wp-i18n', $asset['dependencies'], true ) ) {
277 // Same rule Core applies when registering its own package scripts.
278 // Translations resolve via `{locale}-{handle}.json`, which is keyed
279 // by handle, so the polyfill's own src path does not break the lookup.
280 $scripts->set_translations( $handle );
281 }
282 }
283 }
284
285 /**
286 * Check whether the active Gutenberg plugin can satisfy a forced script.
287 *
288 * @param string|null $minimum_version Minimum Gutenberg version required for the script, or null when any active Gutenberg is sufficient.
289 * @param string|null $gutenberg_version Active Gutenberg version, or null when Gutenberg is inactive.
290 * @return bool True when Gutenberg is active and new enough.
291 */
292 private static function is_gutenberg_version_safe( $minimum_version, $gutenberg_version ) {
293 if ( null === $gutenberg_version ) {
294 return false;
295 }
296
297 if ( null === $minimum_version ) {
298 return true;
299 }
300
301 return version_compare( $gutenberg_version, $minimum_version, '>=' );
302 }
303
304 /**
305 * Register polyfill script modules.
306 *
307 * Call to wp_register_script_module() silently ignores duplicate registrations (first wins),
308 * so no explicit is_registered check is needed.
309 *
310 * @param string $build_dir Absolute path to the build directory.
311 * @param string $base_file File path for plugins_url() computation.
312 */
313 private static function register_modules( $build_dir, $base_file ) {
314 if ( ! function_exists( 'wp_register_script_module' ) ) {
315 return;
316 }
317
318 $modules = array( 'boot', 'route', 'a11y' );
319
320 foreach ( $modules as $name ) {
321 $module_id = '@wordpress/' . $name;
322
323 if ( ! isset( self::$requested[ $module_id ] ) ) {
324 continue;
325 }
326
327 $asset_file = $build_dir . '/modules/' . $name . '/index.asset.php';
328
329 if ( ! file_exists( $asset_file ) ) {
330 continue;
331 }
332
333 $asset = require $asset_file;
334
335 wp_register_script_module(
336 $module_id,
337 plugins_url( 'build/modules/' . $name . '/index.js', $base_file ),
338 $asset['module_dependencies'] ?? array(),
339 $asset['version']
340 );
341 }
342 }
343 }
344