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