PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-assets/src/class-assets.php +827 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,827 @@
1 +<?php
2 +/**
3 + * Jetpack Assets package.
4 + *
5 + * @package automattic/jetpack-assets
6 + */
7 +
8 +namespace Automattic\Jetpack;
9 +
10 +use Automattic\Jetpack\Assets\Semver;
11 +use Automattic\Jetpack\Assets\Shared_Stores_Assets;
12 +use Automattic\Jetpack\Constants as Jetpack_Constants;
13 +use InvalidArgumentException;
14 +
15 +/**
16 + * Class Assets
17 + */
18 +class Assets {
19 + /**
20 + * Holds all the scripts handles that should be loaded in a deferred fashion.
21 + *
22 + * @var array
23 + */
24 + private $defer_script_handles = array();
25 +
26 + /**
27 + * The singleton instance of this class.
28 + *
29 + * @var Assets
30 + */
31 + protected static $instance;
32 +
33 + /**
34 + * The registered textdomain mappings.
35 + *
36 + * @var array `array( mapped_domain => array( string target_domain, string target_type, string semver ) )`.
37 + */
38 + private static $domain_map = array();
39 +
40 + /**
41 + * The registered package paths, by textdomain.
42 + *
43 + * Separate from `$domain_map` because the two answer different questions:
44 + * the map says which domain a package's strings are translated under, while
45 + * this says where the package's files live — the prefix WordPress hashes to
46 + * name a JS translation file. A package whose textdomain is already its
47 + * plugin's has nothing to alias but still needs the path.
48 + *
49 + * Note the entries are keyed by domain, not by script: `downloadI18n()`
50 + * prepends a domain's prefix to every bundle path looked up under it. That
51 + * is only ever one package's path, so a plugin must not both bundle a
52 + * package whose textdomain is the plugin's own and load its own
53 + * `wp-jp-i18n-loader` bundles under that same domain — the package's prefix
54 + * would be applied to the plugin's catalogs too, and they would all 404. No
55 + * plugin does both today. One that needs to should give the package a
56 + * distinct textdomain, the way `jetpack-backup-pkg` and
57 + * `jetpack-videopress-pkg` do.
58 + *
59 + * @var array `array( domain => array( string semver, string path_prefix ) )`.
60 + */
61 + private static $domain_paths = array();
62 +
63 + /**
64 + * Constructor.
65 + *
66 + * Static-only class, so nothing here.
67 + */
68 + private function __construct() {}
69 +
70 + // ////////////////////
71 + // region Async script loading
72 +
73 + /**
74 + * Get the singleton instance of the class.
75 + *
76 + * @return Assets
77 + */
78 + public static function instance() {
79 + if ( ! isset( self::$instance ) ) {
80 + self::$instance = new Assets();
81 + }
82 +
83 + return self::$instance;
84 + }
85 +
86 + /**
87 + * A public method for adding the async script.
88 + *
89 + * @deprecated Since 2.1.0, the `strategy` feature should be used instead, with the "defer" setting.
90 + *
91 + * @param string $script_handle Script handle.
92 + */
93 + public static function add_async_script( $script_handle ) {
94 + _deprecated_function( __METHOD__, '2.1.0' );
95 +
96 + wp_script_add_data( $script_handle, 'strategy', 'defer' );
97 + }
98 +
99 + /**
100 + * Add an async attribute to scripts that can be loaded deferred.
101 + * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script
102 + *
103 + * @deprecated Since 2.1.0, the `strategy` feature should be used instead.
104 + *
105 + * @param string $tag The <script> tag for the enqueued script.
106 + * @param string $handle The script's registered handle.
107 + */
108 + public function script_add_async( $tag, $handle ) {
109 + _deprecated_function( __METHOD__, '2.1.0' );
110 + if ( empty( $this->defer_script_handles ) ) {
111 + return $tag;
112 + }
113 +
114 + if ( in_array( $handle, $this->defer_script_handles, true ) ) {
115 + // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript
116 + return preg_replace( '/<script( [^>]*)? src=/i', '<script defer$1 src=', $tag );
117 + }
118 +
119 + return $tag;
120 + }
121 +
122 + /**
123 + * A helper function that lets you enqueue scripts in an async fashion.
124 + *
125 + * @deprecated Since 2.1.0 - use the strategy feature instead.
126 + *
127 + * @param string $handle Name of the script. Should be unique.
128 + * @param string $min_path Minimized script path.
129 + * @param string $non_min_path Full Script path.
130 + * @param array $deps Array of script dependencies.
131 + * @param bool $ver The script version.
132 + * @param bool $in_footer Should the script be included in the footer.
133 + */
134 + public static function enqueue_async_script( $handle, $min_path, $non_min_path, $deps = array(), $ver = false, $in_footer = true ) {
135 + _deprecated_function( __METHOD__, '2.1.0' );
136 + wp_enqueue_script( $handle, self::get_file_url_for_environment( $min_path, $non_min_path ), $deps, $ver, $in_footer );
137 + wp_script_add_data( $handle, 'strategy', 'defer' );
138 + }
139 +
140 + // endregion .
141 +
142 + // ////////////////////
143 + // region Utils
144 +
145 + /**
146 + * Given a minified path, and a non-minified path, will return
147 + * a minified or non-minified file URL based on whether SCRIPT_DEBUG is set and truthy.
148 + *
149 + * If $package_path is provided, then the minified or non-minified file URL will be generated
150 + * relative to the root package directory.
151 + *
152 + * Both `$min_base` and `$non_min_base` can be either full URLs, or are expected to be relative to the
153 + * root Jetpack directory.
154 + *
155 + * @param string $min_path minified path.
156 + * @param string $non_min_path non-minified path.
157 + * @param string $package_path Optional. A full path to a file inside a package directory
158 + * The URL will be relative to its directory. Default empty.
159 + * Typically this is done by passing __FILE__ as the argument.
160 + *
161 + * @return string The URL to the file
162 + * @since 1.0.3
163 + * @since-jetpack 5.6.0
164 + */
165 + public static function get_file_url_for_environment( $min_path, $non_min_path, $package_path = '' ) {
166 + $path = ( Jetpack_Constants::is_defined( 'SCRIPT_DEBUG' ) && Jetpack_Constants::get_constant( 'SCRIPT_DEBUG' ) )
167 + ? $non_min_path
168 + : $min_path;
169 +
170 + /*
171 + * If the path is actually a full URL, keep that.
172 + * We look for a host value, since enqueues are sometimes without a scheme.
173 + */
174 + $file_parts = wp_parse_url( $path );
175 + if ( ! empty( $file_parts['host'] ) ) {
176 + $url = $path;
177 + } else {
178 + $plugin_path = empty( $package_path ) ? Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) : $package_path;
179 +
180 + $url = plugins_url( $path, $plugin_path );
181 + }
182 +
183 + /**
184 + * Filters the URL for a file passed through the get_file_url_for_environment function.
185 + *
186 + * @since 1.0.3
187 + *
188 + * @package assets
189 + *
190 + * @param string $url The URL to the file.
191 + * @param string $min_path The minified path.
192 + * @param string $non_min_path The non-minified path.
193 + */
194 + return apply_filters( 'jetpack_get_file_for_environment', $url, $min_path, $non_min_path );
195 + }
196 +
197 + /**
198 + * Passes an array of URLs to wp_resource_hints.
199 + *
200 + * @since 1.5.0
201 + *
202 + * @param string|array $urls URLs to hint.
203 + * @param string $type One of the supported resource types: dns-prefetch (default), preconnect, prefetch, or prerender.
204 + */
205 + public static function add_resource_hint( $urls, $type = 'dns-prefetch' ) {
206 + add_filter(
207 + 'wp_resource_hints',
208 + function ( $hints, $resource_type ) use ( $urls, $type ) {
209 + if ( $resource_type === $type ) {
210 + // Type casting to array required since the function accepts a single string.
211 + foreach ( (array) $urls as $url ) {
212 + $hints[] = $url;
213 + }
214 + }
215 + return $hints;
216 + },
217 + 10,
218 + 2
219 + );
220 + }
221 +
222 + /**
223 + * Serve a WordPress.com static resource via a randomized wp.com subdomain.
224 + *
225 + * @since 1.9.0
226 + *
227 + * @param string $url WordPress.com static resource URL.
228 + *
229 + * @return string $url
230 + */
231 + public static function staticize_subdomain( $url ) {
232 + // Extract hostname from URL.
233 + $host = wp_parse_url( $url, PHP_URL_HOST );
234 +
235 + // Explode hostname on '.'.
236 + $exploded_host = explode( '.', $host );
237 +
238 + // Retrieve the name and TLD.
239 + if ( count( $exploded_host ) > 1 ) {
240 + $name = $exploded_host[ count( $exploded_host ) - 2 ];
241 + $tld = $exploded_host[ count( $exploded_host ) - 1 ];
242 + // Rebuild domain excluding subdomains.
243 + $domain = $name . '.' . $tld;
244 + } else {
245 + $domain = $host;
246 + }
247 + // Array of Automattic domains.
248 + $domains_allowed = array( 'wordpress.com', 'wp.com' );
249 +
250 + // Return $url if not an Automattic domain.
251 + if ( ! in_array( $domain, $domains_allowed, true ) ) {
252 + return $url;
253 + }
254 +
255 + if ( \is_ssl() ) {
256 + return preg_replace( '|https?://[^/]++/|', 'https://s-ssl.wordpress.com/', $url );
257 + }
258 +
259 + /*
260 + * Generate a random subdomain id by taking the modulus of the crc32 value of the URL.
261 + * Valid values are 0, 1, and 2.
262 + */
263 + $static_counter = abs( crc32( basename( $url ) ) % 3 );
264 +
265 + return preg_replace( '|://[^/]+?/|', "://s$static_counter.wp.com/", $url );
266 + }
267 +
268 + /**
269 + * Resolve '.' and '..' components in a path or URL.
270 + *
271 + * @since 1.12.0
272 + * @param string $path Path or URL.
273 + * @return string Normalized path or URL.
274 + */
275 + public static function normalize_path( $path ) {
276 + $parts = wp_parse_url( $path );
277 + if ( ! isset( $parts['path'] ) ) {
278 + return $path;
279 + }
280 +
281 + $ret = '';
282 + $ret .= isset( $parts['scheme'] ) ? $parts['scheme'] . '://' : '';
283 + if ( isset( $parts['user'] ) || isset( $parts['pass'] ) ) {
284 + $ret .= $parts['user'] ?? '';
285 + $ret .= isset( $parts['pass'] ) ? ':' . $parts['pass'] : '';
286 + $ret .= '@';
287 + }
288 + $ret .= $parts['host'] ?? '';
289 + $ret .= isset( $parts['port'] ) ? ':' . $parts['port'] : '';
290 +
291 + $pp = explode( '/', $parts['path'] );
292 + if ( '' === $pp[0] ) {
293 + $ret .= '/';
294 + array_shift( $pp );
295 + }
296 + $i = 0;
297 + while ( $i < count( $pp ) ) { // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found
298 + if ( '' === $pp[ $i ] || '.' === $pp[ $i ] || 0 === $i && '..' === $pp[ $i ] ) {
299 + array_splice( $pp, $i, 1 );
300 + } elseif ( '..' === $pp[ $i ] ) {
301 + array_splice( $pp, --$i, 2 );
302 + } else {
303 + ++$i;
304 + }
305 + }
306 + $ret .= implode( '/', $pp );
307 +
308 + $ret .= isset( $parts['query'] ) ? '?' . $parts['query'] : '';
309 + $ret .= isset( $parts['fragment'] ) ? '#' . $parts['fragment'] : '';
310 +
311 + return $ret;
312 + }
313 +
314 + // endregion .
315 +
316 + // ////////////////////
317 + // region Webpack-built script registration
318 +
319 + /**
320 + * Register a Webpack-built script.
321 + *
322 + * Our Webpack-built scripts tend to need a bunch of boilerplate:
323 + * - A call to `Assets::get_file_url_for_environment()` for possible debugging.
324 + * - A call to `wp_register_style()` for extracted CSS, possibly with detection of RTL.
325 + * - Loading of dependencies and version provided by `@wordpress/dependency-extraction-webpack-plugin`.
326 + * - Avoiding WPCom's broken minifier.
327 + *
328 + * This wrapper handles all of that.
329 + *
330 + * @since 1.12.0
331 + * @since 2.1.0 Add a new `strategy` option to leverage WP >= 6.3 script strategy feature. The `async` option is deprecated.
332 + * @param string $handle Name of the script. Should be unique across both scripts and styles.
333 + * @param string $path Minimized script path.
334 + * @param string $relative_to File that `$path` is relative to. Pass `__FILE__`.
335 + * @param array $options Additional options:
336 + * - `asset_path`: (string|null) `.asset.php` to load. Default is to base it on `$path`.
337 + * - `async`: (bool) Set true to register the script as deferred, like `Assets::enqueue_async_script()`. Deprecated in favor of `strategy`.
338 + * - `css_dependencies`: (string[]) Additional style dependencies to queue.
339 + * - `css_path`: (string|null) `.css` to load. Default is to base it on `$path`.
340 + * - `dependencies`: (string[]) Additional script dependencies to queue.
341 + * - `enqueue`: (bool) Set true to enqueue the script immediately.
342 + * - `in_footer`: (bool) Set true to register script for the footer.
343 + * - `media`: (string) Media for the css file. Default 'all'.
344 + * - `minify`: (bool|null) Set true to pass `minify=true` in the query string, or `null` to suppress the normal `minify=false`.
345 + * - `nonmin_path`: (string) Non-minified script path.
346 + * - `strategy`: (string) Specify a script strategy to use, eg. `defer` or `async`. Default is `""`.
347 + * - `textdomain`: (string) Text domain for the script. Required if the script depends on wp-i18n.
348 + * - `version`: (string) Override the version from the `asset_path` file.
349 + * @phan-param array{asset_path?:?string,async?:bool,css_dependencies?:string[],css_path?:?string,dependencies?:string[],enqueue?:bool,in_footer?:bool,media?:string,minify?:?bool,nonmin_path?:string,strategy?:string,textdomain?:string,version?:string} $options
350 + * @throws \InvalidArgumentException If arguments are invalid.
351 + */
352 + public static function register_script( $handle, $path, $relative_to, array $options = array() ) {
353 + if ( substr( $path, -3 ) !== '.js' ) {
354 + throw new \InvalidArgumentException( '$path must end in ".js"' );
355 + }
356 +
357 + if ( isset( $options['async'] ) ) {
358 + _deprecated_argument( __METHOD__, '2.1.0', 'The `async` option is deprecated in favor of `strategy`' );
359 + }
360 +
361 + $dir = dirname( $relative_to );
362 + $base = substr( $path, 0, -3 );
363 + $options += array(
364 + 'asset_path' => "$base.asset.php",
365 + 'async' => false,
366 + 'css_dependencies' => array(),
367 + 'css_path' => "$base.css",
368 + 'dependencies' => array(),
369 + 'enqueue' => false,
370 + 'in_footer' => false,
371 + 'media' => 'all',
372 + 'minify' => false,
373 + 'strategy' => '',
374 + 'textdomain' => null,
375 + );
376 + '@phan-var array{asset_path:?string,async:bool,css_dependencies:string[],css_path:?string,dependencies:string[],enqueue:bool,in_footer:bool,media:string,minify:?bool,nonmin_path?:string,strategy:string,textdomain:string,version?:string} $options'; // Phan gets confused by the array addition.
377 +
378 + if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && substr( $options['css_path'], -4 ) !== '.css' ) {
379 + throw new \InvalidArgumentException( '$options[\'css_path\'] must end in ".css"' );
380 + }
381 +
382 + if ( isset( $options['nonmin_path'] ) ) {
383 + $url = self::get_file_url_for_environment( $path, $options['nonmin_path'], $relative_to );
384 + } else {
385 + $url = plugins_url( $path, $relative_to );
386 + }
387 + $url = self::normalize_path( $url );
388 + if ( null !== $options['minify'] ) {
389 + $url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
390 + }
391 +
392 + if ( $options['asset_path'] && file_exists( "$dir/{$options['asset_path']}" ) ) {
393 + $asset = require "$dir/{$options['asset_path']}"; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath
394 + $options['dependencies'] = array_merge( $asset['dependencies'], $options['dependencies'] );
395 + $options['css_dependencies'] = array_merge(
396 + array_filter(
397 + $asset['dependencies'],
398 + function ( $d ) {
399 + return wp_style_is( $d, 'registered' );
400 + }
401 + ),
402 + $options['css_dependencies']
403 + );
404 + $ver = $options['version'] ?? $asset['version'];
405 + } else {
406 + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
407 + $ver = $options['version'] ?? @filemtime( "$dir/$path" );
408 + }
409 +
410 + if ( $options['async'] && '' === $options['strategy'] ) { // Handle the deprecated `async` option
411 + $options['strategy'] = 'defer';
412 + }
413 + wp_register_script(
414 + $handle,
415 + $url,
416 + $options['dependencies'],
417 + $ver,
418 + array(
419 + 'in_footer' => $options['in_footer'],
420 + 'strategy' => $options['strategy'],
421 + )
422 + );
423 +
424 + if ( $options['textdomain'] ) {
425 + // phpcs:ignore Jetpack.Functions.I18n.DomainNotLiteral
426 + wp_set_script_translations( $handle, $options['textdomain'] );
427 + } elseif ( in_array( 'wp-i18n', $options['dependencies'], true ) ) {
428 + _doing_it_wrong(
429 + __METHOD__,
430 + /* translators: %s is the script handle. */
431 + esc_html( sprintf( __( 'Script "%s" depends on wp-i18n but does not specify "textdomain"', 'jetpack-assets' ), $handle ) ),
432 + ''
433 + );
434 + }
435 +
436 + if ( is_string( $options['css_path'] ) && $options['css_path'] !== '' && file_exists( "$dir/{$options['css_path']}" ) ) {
437 + $csspath = $options['css_path'];
438 + if ( is_rtl() ) {
439 + $rtlcsspath = substr( $csspath, 0, -4 ) . '.rtl.css';
440 + if ( file_exists( "$dir/$rtlcsspath" ) ) {
441 + $csspath = $rtlcsspath;
442 + }
443 + }
444 +
445 + $url = self::normalize_path( plugins_url( $csspath, $relative_to ) );
446 + if ( null !== $options['minify'] ) {
447 + $url = add_query_arg( 'minify', $options['minify'] ? 'true' : 'false', $url );
448 + }
449 + wp_register_style( $handle, $url, $options['css_dependencies'], $ver, $options['media'] );
450 + wp_script_add_data( $handle, 'Jetpack::Assets::hascss', true );
451 + } else {
452 + wp_script_add_data( $handle, 'Jetpack::Assets::hascss', false );
453 + }
454 +
455 + if ( $options['enqueue'] ) {
456 + self::enqueue_script( $handle );
457 + }
458 + }
459 +
460 + /**
461 + * Enqueue a script registered with `Assets::register_script`.
462 + *
463 + * @since 1.12.0
464 + * @param string $handle Name of the script. Should be unique across both scripts and styles.
465 + */
466 + public static function enqueue_script( $handle ) {
467 + wp_enqueue_script( $handle );
468 + if ( wp_scripts()->get_data( $handle, 'Jetpack::Assets::hascss' ) ) {
469 + wp_enqueue_style( $handle );
470 + }
471 + }
472 +
473 + /**
474 + * Re-hook the bootstraps an older copy's `actions.php` did not know about. See JETPACK-2649.
475 + *
476 + * Static callables only: `add_action()` dedupes those, but closures and object callables would
477 + * register twice. Callers must run before `wp_loaded`.
478 + *
479 + * @access private
480 + * @since 5.0.5
481 + */
482 + public static function ensure_package_bootstrap() {
483 + Shared_Stores_Assets::configure();
484 + }
485 +
486 + /**
487 + * 'wp_default_scripts' action handler.
488 + *
489 + * This registers the `wp-jp-i18n-loader` script for use by Webpack bundles built with
490 + * `@automattic/i18n-loader-webpack-plugin`.
491 + *
492 + * @since 1.14.0
493 + * @param \WP_Scripts $wp_scripts WP_Scripts instance.
494 + */
495 + public static function wp_default_scripts_hook( $wp_scripts ) {
496 + $data = array(
497 + 'baseUrl' => false,
498 + 'locale' => determine_locale(),
499 + 'domainMap' => array(),
500 + 'domainPaths' => array(),
501 + );
502 +
503 + $lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
504 + $content_dir = Jetpack_Constants::get_constant( 'WP_CONTENT_DIR' );
505 + $abspath = Jetpack_Constants::get_constant( 'ABSPATH' );
506 +
507 + // Note: str_starts_with() is not used here, as wp-includes/compat.php may not be loaded at this point.
508 + if ( strpos( $lang_dir, $content_dir ) === 0 ) {
509 + $data['baseUrl'] = content_url( substr( trailingslashit( $lang_dir ), strlen( trailingslashit( $content_dir ) ) ) );
510 + } elseif ( strpos( $lang_dir, $abspath ) === 0 ) {
511 + $data['baseUrl'] = site_url( substr( trailingslashit( $lang_dir ), strlen( untrailingslashit( $abspath ) ) ) );
512 + }
513 +
514 + foreach ( self::$domain_map as $from => list( $to, $type ) ) {
515 + $data['domainMap'][ $from ] = ( 'core' === $type ? '' : "{$type}/" ) . $to;
516 + }
517 + foreach ( self::$domain_paths as $from => list( , $path ) ) {
518 + if ( '' !== $path ) {
519 + $data['domainPaths'][ $from ] = trailingslashit( $path );
520 + }
521 + }
522 +
523 + /**
524 + * Filters the i18n state data for use by Webpack bundles built with
525 + * `@automattic/i18n-loader-webpack-plugin`.
526 + *
527 + * @since 1.14.0
528 + * @package assets
529 + * @param array $data The state data to generate. Expected fields are:
530 + * - `baseUrl`: (string|false) The URL to the languages directory. False if no URL could be determined.
531 + * - `locale`: (string) The locale for the page.
532 + * - `domainMap`: (string[]) A mapping from Composer package textdomains to the corresponding
533 + * `plugins/textdomain` or `themes/textdomain` (or core `textdomain`, but that's unlikely).
534 + * - `domainPaths`: (string[]) A mapping from Composer package textdomains to the corresponding package
535 + * paths.
536 + */
537 + $data = apply_filters( 'jetpack_i18n_state', $data );
538 +
539 + // Can't use self::register_script(), this action is called too early.
540 + if ( file_exists( __DIR__ . '/../build/i18n-loader.asset.php' ) ) {
541 + $path = '../build/i18n-loader.js';
542 + $asset = require __DIR__ . '/../build/i18n-loader.asset.php';
543 + } else {
544 + $path = 'js/i18n-loader.js';
545 + $asset = array(
546 + 'dependencies' => array( 'wp-i18n' ),
547 + 'version' => filemtime( __DIR__ . "/$path" ),
548 + );
549 + }
550 + $url = self::normalize_path( plugins_url( $path, __FILE__ ) );
551 + $url = add_query_arg( 'minify', 'true', $url );
552 +
553 + $handle = 'wp-jp-i18n-loader';
554 +
555 + $wp_scripts->add( $handle, $url, $asset['dependencies'], $asset['version'] );
556 +
557 + // Ensure the script is loaded in the footer and deferred.
558 + $wp_scripts->add_data( $handle, 'group', 1 );
559 +
560 + if ( ! is_array( $data ) ||
561 + ! isset( $data['baseUrl'] ) || ! ( is_string( $data['baseUrl'] ) || false === $data['baseUrl'] ) ||
562 + ! isset( $data['locale'] ) || ! is_string( $data['locale'] ) ||
563 + ! isset( $data['domainMap'] ) || ! is_array( $data['domainMap'] ) ||
564 + ! isset( $data['domainPaths'] ) || ! is_array( $data['domainPaths'] )
565 + ) {
566 + $wp_scripts->add_inline_script( $handle, 'console.warn( "I18n state deleted by jetpack_i18n_state hook" );' );
567 + } elseif ( ! $data['baseUrl'] ) {
568 + $wp_scripts->add_inline_script( $handle, 'console.warn( "Failed to determine languages base URL. Is WP_LANG_DIR in the WordPress root?" );' );
569 + } else {
570 + $data['domainMap'] = (object) $data['domainMap']; // Ensure it becomes a json object.
571 + $data['domainPaths'] = (object) $data['domainPaths']; // Ensure it becomes a json object.
572 + $wp_scripts->add_inline_script( $handle, 'wp.jpI18nLoader.state = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';' );
573 + }
574 +
575 + // Deprecated state module: Depend on wp-i18n to ensure global `wp` exists and because anything needing this will need that too.
576 + $wp_scripts->add( 'wp-jp-i18n-state', false, array( 'wp-deprecated', $handle ) );
577 + $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.deprecated( "wp-jp-i18n-state", { alternative: "wp-jp-i18n-loader" } );' );
578 + $wp_scripts->add_inline_script( 'wp-jp-i18n-state', 'wp.jpI18nState = wp.jpI18nLoader.state;' );
579 + }
580 +
581 + // endregion .
582 +
583 + // ////////////////////
584 + // region Textdomain aliasing
585 +
586 + /**
587 + * Register a textdomain alias.
588 + *
589 + * Composer packages included in plugins will likely not use the textdomain of the plugin, while
590 + * WordPress's i18n infrastructure will include the translations in the plugin's domain. This
591 + * allows for mapping the package's domain to the plugin's.
592 + *
593 + * Since multiple plugins may use the same package, we include the package's version here so
594 + * as to choose the most recent translations (which are most likely to match the package
595 + * selected by jetpack-autoloader).
596 + *
597 + * @since 1.15.0
598 + * @param string $from Domain to alias.
599 + * @param string $to Domain to alias it to.
600 + * @param string $totype What is the target of the alias: 'plugins', 'themes', or 'core'.
601 + * @param string $ver Version of the `$from` domain.
602 + * @param string $path Path to prepend when lazy-loading from JavaScript.
603 + * @throws InvalidArgumentException If arguments are invalid.
604 + */
605 + public static function alias_textdomain( $from, $to, $totype, $ver, $path = '' ) {
606 + if ( ! in_array( $totype, array( 'plugins', 'themes', 'core' ), true ) ) {
607 + throw new InvalidArgumentException( 'Type must be "plugins", "themes", or "core"' );
608 + }
609 +
610 + if (
611 + did_action( 'wp_default_scripts' ) &&
612 + // Don't complain during plugin activation.
613 + ! defined( 'WP_SANDBOX_SCRAPING' )
614 + ) {
615 + _doing_it_wrong(
616 + __METHOD__,
617 + sprintf(
618 + /* translators: 1: wp_default_scripts. 2: Name of the domain being aliased. */
619 + esc_html__( 'Textdomain aliases should be registered before the %1$s hook. This notice was triggered by the %2$s domain.', 'jetpack-assets' ),
620 + '<code>wp_default_scripts</code>',
621 + '<code>' . esc_html( $from ) . '</code>'
622 + ),
623 + ''
624 + );
625 + }
626 +
627 + // Where the package lives is needed for JS translation files whether or
628 + // not its domain is aliased, so it is recorded before the self-alias
629 + // check below.
630 + if (
631 + empty( self::$domain_paths[ $from ] ) ||
632 + Semver::compare( $ver, self::$domain_paths[ $from ][0] ) > 0
633 + ) {
634 + self::$domain_paths[ $from ] = array( $ver, $path );
635 + }
636 +
637 + // A self-alias would make filter_gettext() re-translate into the same
638 + // domain, recursing infinitely on any untranslated string (a package
639 + // textdomain can collide with its containing plugin's slug).
640 + if ( $from === $to ) {
641 + return;
642 + }
643 +
644 + if ( empty( self::$domain_map[ $from ] ) ) {
645 + self::init_domain_map_hooks( $from, array() === self::$domain_map );
646 + self::$domain_map[ $from ] = array( $to, $totype, $ver );
647 + } elseif ( Semver::compare( $ver, self::$domain_map[ $from ][2] ) > 0 ) {
648 + self::$domain_map[ $from ] = array( $to, $totype, $ver );
649 + }
650 + }
651 +
652 + /**
653 + * Register textdomain aliases from a mapping file.
654 + *
655 + * The mapping file is simply a PHP file that returns an array
656 + * with the following properties:
657 + * - 'domain': String, `$to`
658 + * - 'type': String, `$totype`
659 + * - 'packages': Array, mapping `$from` to `array( 'path' => $path, 'ver' => $ver )` (or to the string `$ver` for back compat).
660 + * - 'paths': Array, same shape, for packages whose textdomain is already
661 + * `$to`. Those must not be aliased — that would recurse — but their
662 + * paths are still needed to locate their JavaScript translations.
663 + *
664 + * @since 1.15.0
665 + * @param string $file Mapping file.
666 + */
667 + public static function alias_textdomains_from_file( $file ) {
668 + $data = require $file;
669 + foreach ( $data['packages'] as $from => $fromdata ) {
670 + if ( ! is_array( $fromdata ) ) {
671 + $fromdata = array(
672 + 'path' => '',
673 + 'ver' => $fromdata,
674 + );
675 + }
676 + self::alias_textdomain( $from, $data['domain'], $data['type'], $fromdata['ver'], $fromdata['path'] );
677 + }
678 + // Aliasing a domain to itself is a no-op that `alias_textdomain()`
679 + // declines, leaving just the path registration these entries are for.
680 + foreach ( $data['paths'] ?? array() as $from => $fromdata ) {
681 + self::alias_textdomain( $from, $from, $data['type'], $fromdata['ver'], $fromdata['path'] );
682 + }
683 + }
684 +
685 + /**
686 + * Register the hooks for textdomain aliasing.
687 + *
688 + * @param string $domain Domain to alias.
689 + * @param bool $firstcall If this is the first call.
690 + */
691 + private static function init_domain_map_hooks( $domain, $firstcall ) {
692 + // If WordPress's plugin API is available already, use it. If not,
693 + // drop data into `$wp_filter` for `WP_Hook::build_preinitialized_hooks()`.
694 + if ( function_exists( 'add_filter' ) ) {
695 + $add_filter = 'add_filter';
696 + } else {
697 + $add_filter = function ( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
698 + global $wp_filter;
699 + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
700 + $wp_filter[ $hook_name ][ $priority ][] = array(
701 + 'accepted_args' => $accepted_args,
702 + 'function' => $callback,
703 + );
704 + };
705 + }
706 +
707 + $add_filter( "gettext_{$domain}", array( self::class, 'filter_gettext' ), 10, 3 );
708 + $add_filter( "ngettext_{$domain}", array( self::class, 'filter_ngettext' ), 10, 5 );
709 + $add_filter( "gettext_with_context_{$domain}", array( self::class, 'filter_gettext_with_context' ), 10, 4 );
710 + $add_filter( "ngettext_with_context_{$domain}", array( self::class, 'filter_ngettext_with_context' ), 10, 6 );
711 + if ( $firstcall ) {
712 + $add_filter( 'load_script_translation_file', array( self::class, 'filter_load_script_translation_file' ), 10, 3 );
713 + }
714 + }
715 +
716 + /**
717 + * Filter for `gettext`.
718 + *
719 + * @since 1.15.0
720 + * @param string $translation Translated text.
721 + * @param string $text Text to translate.
722 + * @param string $domain Text domain.
723 + * @return string Translated text.
724 + */
725 + public static function filter_gettext( $translation, $text, $domain ) {
726 + if ( $translation === $text ) {
727 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
728 + $newtext = __( $text, self::$domain_map[ $domain ][0] );
729 + if ( $newtext !== $text ) {
730 + return $newtext;
731 + }
732 + }
733 + return $translation;
734 + }
735 +
736 + /**
737 + * Filter for `ngettext`.
738 + *
739 + * @since 1.15.0
740 + * @param string $translation Translated text.
741 + * @param string $single The text to be used if the number is singular.
742 + * @param string $plural The text to be used if the number is plural.
743 + * @param int $number The number to compare against to use either the singular or plural form.
744 + * @param string $domain Text domain.
745 + * @return string Translated text.
746 + */
747 + public static function filter_ngettext( $translation, $single, $plural, $number, $domain ) {
748 + if ( $translation === $single || $translation === $plural ) {
749 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
750 + $translation = _n( $single, $plural, $number, self::$domain_map[ $domain ][0] );
751 + }
752 + return $translation;
753 + }
754 +
755 + /**
756 + * Filter for `gettext_with_context`.
757 + *
758 + * @since 1.15.0
759 + * @param string $translation Translated text.
760 + * @param string $text Text to translate.
761 + * @param string $context Context information for the translators.
762 + * @param string $domain Text domain.
763 + * @return string Translated text.
764 + */
765 + public static function filter_gettext_with_context( $translation, $text, $context, $domain ) {
766 + if ( $translation === $text ) {
767 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
768 + $translation = _x( $text, $context, self::$domain_map[ $domain ][0] );
769 + }
770 + return $translation;
771 + }
772 +
773 + /**
774 + * Filter for `ngettext_with_context`.
775 + *
776 + * @since 1.15.0
777 + * @param string $translation Translated text.
778 + * @param string $single The text to be used if the number is singular.
779 + * @param string $plural The text to be used if the number is plural.
780 + * @param int $number The number to compare against to use either the singular or plural form.
781 + * @param string $context Context information for the translators.
782 + * @param string $domain Text domain.
783 + * @return string Translated text.
784 + */
785 + public static function filter_ngettext_with_context( $translation, $single, $plural, $number, $context, $domain ) {
786 + if ( $translation === $single || $translation === $plural ) {
787 + // phpcs:ignore WordPress.WP.I18n -- This is a filter hook to map the text domains from our Composer packages to the domain for a containing plugin. See https://wp.me/p2gHKz-oRh#problem-6-text-domains-in-composer-packages
788 + $translation = _nx( $single, $plural, $number, $context, self::$domain_map[ $domain ][0] );
789 + }
790 + return $translation;
791 + }
792 +
793 + /**
794 + * Filter for `load_script_translation_file`.
795 + *
796 + * @since 1.15.0
797 + * @param string|false $file Path to the translation file to load. False if there isn't one.
798 + * @param string $handle Name of the script to register a translation domain to.
799 + * @param string $domain The text domain.
800 + */
801 + public static function filter_load_script_translation_file( $file, $handle, $domain ) {
802 + if ( false !== $file && isset( self::$domain_map[ $domain ] ) && ! is_readable( $file ) ) {
803 + // Determine the part of the filename after the domain.
804 + $suffix = basename( $file );
805 + $l = strlen( $domain );
806 + if ( substr( $suffix, 0, $l ) !== $domain || '-' !== $suffix[ $l ] ) {
807 + return $file;
808 + }
809 + $suffix = substr( $suffix, $l );
810 + $lang_dir = Jetpack_Constants::get_constant( 'WP_LANG_DIR' );
811 +
812 + // Look for replacement files.
813 + list( $newdomain, $type ) = self::$domain_map[ $domain ];
814 + $newfile = $lang_dir . ( 'core' === $type ? '/' : "/{$type}/" ) . $newdomain . $suffix;
815 + if ( is_readable( $newfile ) ) {
816 + return $newfile;
817 + }
818 + }
819 + return $file;
820 + }
821 +
822 + // endregion .
823 +}
824 +
825 +// Enable section folding in vim:
826 +// vim: foldmarker=//\ region,//\ endregion foldmethod=marker
827 +// .