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