| @@ -1,0 +1,351 @@ | ||
| 1 | +<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName | |
| 2 | +/** | |
| 3 | + * Module Name: Asset CDN | |
| 4 | + * Module Description: Serve static files like CSS and JS from Jetpack’s global CDN for faster load times. | |
| 5 | + * Sort Order: 26 | |
| 6 | + * Recommendation Order: 1 | |
| 7 | + * First Introduced: 6.6 | |
| 8 | + * Requires Connection: No | |
| 9 | + * Auto Activate: No | |
| 10 | + * Module Tags: Photos and Videos, Appearance, Recommended | |
| 11 | + * Feature: Recommended, Appearance | |
| 12 | + * Additional Search Queries: site accelerator, accelerate, static, assets, javascript, css, files, performance, cdn, bandwidth, content delivery network, pagespeed, combine js, optimize css | |
| 13 | + * | |
| 14 | + * @package automattic/jetpack | |
| 15 | + */ | |
| 16 | + | |
| 17 | +use Automattic\Jetpack\Assets; | |
| 18 | + | |
| 19 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 20 | + exit( 0 ); | |
| 21 | +} | |
| 22 | + | |
| 23 | +$GLOBALS['concatenate_scripts'] = false; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited | |
| 24 | + | |
| 25 | +Assets::add_resource_hint( '//c0.wp.com', 'preconnect' ); | |
| 26 | + | |
| 27 | +/** | |
| 28 | + * Asset CDN module main class file. | |
| 29 | + */ | |
| 30 | +class Jetpack_Photon_Static_Assets_CDN { | |
| 31 | + const CDN = 'https://c0.wp.com/'; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * Sets up action handlers needed for Jetpack CDN. | |
| 35 | + */ | |
| 36 | + public static function go() { | |
| 37 | + add_action( 'wp_print_scripts', array( __CLASS__, 'cdnize_assets' ) ); | |
| 38 | + add_action( 'wp_print_styles', array( __CLASS__, 'cdnize_assets' ) ); | |
| 39 | + add_action( 'admin_print_scripts', array( __CLASS__, 'cdnize_assets' ) ); | |
| 40 | + add_action( 'admin_print_styles', array( __CLASS__, 'cdnize_assets' ) ); | |
| 41 | + add_action( 'wp_footer', array( __CLASS__, 'cdnize_assets' ) ); | |
| 42 | + add_filter( 'load_script_textdomain_relative_path', array( __CLASS__, 'fix_script_relative_path' ), 10, 2 ); | |
| 43 | + add_filter( 'load_script_translation_file', array( __CLASS__, 'fix_local_script_translation_path' ), 10, 3 ); | |
| 44 | + } | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * Sets up CDN URLs for assets that are enqueued by the WordPress Core. | |
| 48 | + */ | |
| 49 | + public static function cdnize_assets() { | |
| 50 | + global $wp_scripts, $wp_styles, $wp_version; | |
| 51 | + | |
| 52 | + /* | |
| 53 | + * Short-circuit if AMP since not relevant as custom JS is not allowed and CSS is inlined. | |
| 54 | + * Note that it is not suitable to use the jetpack_force_disable_site_accelerator filter for this | |
| 55 | + * because it will be applied before the wp action, which is the point at which the queried object | |
| 56 | + * is available and we know whether the response will be AMP or not. This is particularly important | |
| 57 | + * for AMP-first (native AMP) pages where there are no AMP-specific URLs. | |
| 58 | + */ | |
| 59 | + if ( class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request() ) { | |
| 60 | + return; | |
| 61 | + } | |
| 62 | + | |
| 63 | + /** | |
| 64 | + * Filters Jetpack CDN's Core version number and locale. Can be used to override the values | |
| 65 | + * that Jetpack uses to retrieve assets. Expects the values to be returned in an array. | |
| 66 | + * | |
| 67 | + * @module photon-cdn | |
| 68 | + * | |
| 69 | + * @since 6.6.0 | |
| 70 | + * | |
| 71 | + * @param array $values array( $version = core assets version, i.e. 4.9.8, $locale = desired locale ) | |
| 72 | + */ | |
| 73 | + list( $version, $locale ) = apply_filters( // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable | |
| 74 | + 'jetpack_cdn_core_version_and_locale', | |
| 75 | + array( $wp_version, get_locale() ) | |
| 76 | + ); | |
| 77 | + | |
| 78 | + if ( self::is_public_version( $version ) ) { | |
| 79 | + $site_url = trailingslashit( site_url() ); | |
| 80 | + if ( $wp_scripts instanceof WP_Scripts && is_array( $wp_scripts->registered ) ) { | |
| 81 | + foreach ( $wp_scripts->registered as $handle => $thing ) { | |
| 82 | + if ( wp_startswith( $thing->src, self::CDN ) ) { | |
| 83 | + continue; | |
| 84 | + } | |
| 85 | + if ( ! is_string( $thing->src ) ) { | |
| 86 | + continue; | |
| 87 | + } | |
| 88 | + $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' ); | |
| 89 | + if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ), true ) ) { | |
| 90 | + $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src ); | |
| 91 | + $wp_scripts->registered[ $handle ]->ver = null; | |
| 92 | + } | |
| 93 | + } | |
| 94 | + } | |
| 95 | + if ( $wp_styles instanceof WP_Styles && is_array( $wp_styles->registered ) ) { | |
| 96 | + foreach ( $wp_styles->registered as $handle => $thing ) { | |
| 97 | + if ( wp_startswith( $thing->src, self::CDN ) ) { | |
| 98 | + continue; | |
| 99 | + } | |
| 100 | + if ( ! is_string( $thing->src ) ) { | |
| 101 | + continue; | |
| 102 | + } | |
| 103 | + $src = ltrim( str_replace( $site_url, '', $thing->src ), '/' ); | |
| 104 | + if ( self::is_js_or_css_file( $src ) && in_array( substr( $src, 0, 9 ), array( 'wp-admin/', 'wp-includ' ), true ) ) { | |
| 105 | + $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'c/%1$s/%2$s', $version, $src ); | |
| 106 | + $wp_styles->registered[ $handle ]->ver = null; | |
| 107 | + } | |
| 108 | + } | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + self::cdnize_plugin_assets( 'jetpack', JETPACK__VERSION ); | |
| 113 | + if ( class_exists( 'WooCommerce' ) ) { | |
| 114 | + self::cdnize_plugin_assets( 'woocommerce', WC_VERSION ); | |
| 115 | + } | |
| 116 | + } | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * Ensure use of the correct relative path when determining the JavaScript file names. | |
| 120 | + * | |
| 121 | + * @param string $relative The relative path of the script. False if it could not be determined. | |
| 122 | + * @param string $src The full source url of the script. | |
| 123 | + * @return string The expected relative path for the CDN-ed URL. | |
| 124 | + */ | |
| 125 | + public static function fix_script_relative_path( $relative, $src ) { | |
| 126 | + | |
| 127 | + // Note relevant in AMP responses. See note above. | |
| 128 | + if ( class_exists( Jetpack_AMP_Support::class ) && Jetpack_AMP_Support::is_amp_request() ) { | |
| 129 | + return $relative; | |
| 130 | + } | |
| 131 | + | |
| 132 | + $strpos = strpos( $src, '/wp-includes/' ); | |
| 133 | + | |
| 134 | + // We only treat URLs that have wp-includes in them. Cases like language textdomains | |
| 135 | + // can also use this filter, they don't need to be touched because they are local paths. | |
| 136 | + if ( false !== $strpos ) { | |
| 137 | + return substr( $src, 1 + $strpos ); | |
| 138 | + } | |
| 139 | + | |
| 140 | + // Get the local path from a URL which was CDN'ed by cdnize_plugin_assets(). | |
| 141 | + if ( preg_match( '#^' . preg_quote( self::CDN, '#' ) . 'p/[^/]+/[^/]+/(.*)$#', $src, $m ) ) { | |
| 142 | + return $m[1]; | |
| 143 | + } | |
| 144 | + | |
| 145 | + return $relative; | |
| 146 | + } | |
| 147 | + | |
| 148 | + /** | |
| 149 | + * Ensure use of the correct local path when loading the JavaScript translation file for a CDN'ed asset. | |
| 150 | + * | |
| 151 | + * @param string|false $file Path to the translation file to load. False if there isn't one. | |
| 152 | + * @param string $handle The script handle. | |
| 153 | + * @param string $domain The text domain. | |
| 154 | + * | |
| 155 | + * @return string The transformed local languages path. | |
| 156 | + */ | |
| 157 | + public static function fix_local_script_translation_path( $file, $handle, $domain ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable | |
| 158 | + global $wp_scripts; | |
| 159 | + | |
| 160 | + // This is a rewritten plugin URL, so load the language file from the plugins path. | |
| 161 | + if ( $file && isset( $wp_scripts->registered[ $handle ] ) && wp_startswith( $wp_scripts->registered[ $handle ]->src, self::CDN . 'p' ) ) { | |
| 162 | + return WP_LANG_DIR . '/plugins/' . basename( $file ); | |
| 163 | + } | |
| 164 | + | |
| 165 | + return $file; | |
| 166 | + } | |
| 167 | + | |
| 168 | + /** | |
| 169 | + * Sets up CDN URLs for supported plugin assets. | |
| 170 | + * | |
| 171 | + * @param String $plugin_slug plugin slug string. | |
| 172 | + * @param String $current_version plugin version string. | |
| 173 | + * @return null|bool | |
| 174 | + */ | |
| 175 | + public static function cdnize_plugin_assets( $plugin_slug, $current_version ) { | |
| 176 | + global $wp_scripts, $wp_styles; | |
| 177 | + | |
| 178 | + /** | |
| 179 | + * Filters Jetpack CDN's plugin slug and version number. Can be used to override the values | |
| 180 | + * that Jetpack uses to retrieve assets. For example, when testing a development version of Jetpack | |
| 181 | + * the assets are not yet published, so you may need to override the version value to either | |
| 182 | + * trunk, or the latest available version. Expects the values to be returned in an array. | |
| 183 | + * | |
| 184 | + * @module photon-cdn | |
| 185 | + * | |
| 186 | + * @since 6.6.0 | |
| 187 | + * | |
| 188 | + * @param array $values array( $slug = the plugin repository slug, i.e. jetpack, $version = the plugin version, i.e. 6.6 ) | |
| 189 | + */ | |
| 190 | + list( $plugin_slug, $current_version ) = apply_filters( | |
| 191 | + 'jetpack_cdn_plugin_slug_and_version', | |
| 192 | + array( $plugin_slug, $current_version ) | |
| 193 | + ); | |
| 194 | + | |
| 195 | + $assets = self::get_plugin_assets( $plugin_slug, $current_version ); | |
| 196 | + $plugin_directory_url = plugins_url() . '/' . $plugin_slug . '/'; | |
| 197 | + | |
| 198 | + if ( is_wp_error( $assets ) || ! is_array( $assets ) ) { | |
| 199 | + return false; | |
| 200 | + } | |
| 201 | + | |
| 202 | + if ( $wp_scripts instanceof WP_Scripts && is_array( $wp_scripts->registered ) ) { | |
| 203 | + foreach ( $wp_scripts->registered as $handle => $thing ) { | |
| 204 | + if ( wp_startswith( $thing->src, self::CDN ) ) { | |
| 205 | + continue; | |
| 206 | + } | |
| 207 | + if ( wp_startswith( $thing->src, $plugin_directory_url ) ) { | |
| 208 | + $local_path = substr( $thing->src, strlen( $plugin_directory_url ) ); | |
| 209 | + if ( in_array( $local_path, $assets, true ) ) { | |
| 210 | + $wp_scripts->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path ); | |
| 211 | + $wp_scripts->registered[ $handle ]->ver = null; | |
| 212 | + } | |
| 213 | + } | |
| 214 | + } | |
| 215 | + } | |
| 216 | + if ( $wp_styles instanceof WP_Styles && is_array( $wp_styles->registered ) ) { | |
| 217 | + foreach ( $wp_styles->registered as $handle => $thing ) { | |
| 218 | + if ( wp_startswith( $thing->src, self::CDN ) ) { | |
| 219 | + continue; | |
| 220 | + } | |
| 221 | + if ( wp_startswith( $thing->src, $plugin_directory_url ) ) { | |
| 222 | + $local_path = substr( $thing->src, strlen( $plugin_directory_url ) ); | |
| 223 | + if ( in_array( $local_path, $assets, true ) ) { | |
| 224 | + $wp_styles->registered[ $handle ]->src = sprintf( self::CDN . 'p/%1$s/%2$s/%3$s', $plugin_slug, $current_version, $local_path ); | |
| 225 | + $wp_styles->registered[ $handle ]->ver = null; | |
| 226 | + } | |
| 227 | + } | |
| 228 | + } | |
| 229 | + } | |
| 230 | + } | |
| 231 | + | |
| 232 | + /** | |
| 233 | + * Returns cdn-able assets for a given plugin. | |
| 234 | + * | |
| 235 | + * @param string $plugin plugin slug string. | |
| 236 | + * @param string $version plugin version number string. | |
| 237 | + * @return array|bool Will return false if not a public version. | |
| 238 | + */ | |
| 239 | + public static function get_plugin_assets( $plugin, $version ) { | |
| 240 | + if ( 'jetpack' === $plugin && JETPACK__VERSION === $version ) { | |
| 241 | + if ( ! self::is_public_version( $version ) || ! file_exists( JETPACK__PLUGIN_DIR . 'modules/photon-cdn/jetpack-manifest.php' ) ) { | |
| 242 | + return false; | |
| 243 | + } | |
| 244 | + | |
| 245 | + $assets = array(); // The variable will be redefined in the included file. | |
| 246 | + | |
| 247 | + include JETPACK__PLUGIN_DIR . 'modules/photon-cdn/jetpack-manifest.php'; | |
| 248 | + return $assets; | |
| 249 | + } | |
| 250 | + | |
| 251 | + /** | |
| 252 | + * Used for other plugins to provide their bundled assets via filter to | |
| 253 | + * prevent the need of storing them in an option or an external api request | |
| 254 | + * to w.org. | |
| 255 | + * | |
| 256 | + * @module photon-cdn | |
| 257 | + * | |
| 258 | + * @since 6.6.0 | |
| 259 | + * | |
| 260 | + * @param array $assets The assets array for the plugin. | |
| 261 | + * @param string $version The version of the plugin being requested. | |
| 262 | + */ | |
| 263 | + $assets = apply_filters( "jetpack_cdn_plugin_assets-{$plugin}", null, $version ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores | |
| 264 | + if ( is_array( $assets ) ) { | |
| 265 | + return $assets; | |
| 266 | + } | |
| 267 | + | |
| 268 | + if ( ! self::is_public_version( $version ) ) { | |
| 269 | + return false; | |
| 270 | + } | |
| 271 | + | |
| 272 | + $cache = Jetpack_Options::get_option( 'static_asset_cdn_files', array() ); | |
| 273 | + if ( isset( $cache[ $plugin ][ $version ] ) ) { | |
| 274 | + if ( is_array( $cache[ $plugin ][ $version ] ) ) { | |
| 275 | + return $cache[ $plugin ][ $version ]; | |
| 276 | + } | |
| 277 | + if ( is_numeric( $cache[ $plugin ][ $version ] ) ) { | |
| 278 | + // Cache an empty result for up to 24h. | |
| 279 | + if ( (int) $cache[ $plugin ][ $version ] + DAY_IN_SECONDS > time() ) { | |
| 280 | + return array(); | |
| 281 | + } | |
| 282 | + } | |
| 283 | + } | |
| 284 | + | |
| 285 | + $url = sprintf( 'http://downloads.wordpress.org/plugin-checksums/%s/%s.json', $plugin, $version ); | |
| 286 | + | |
| 287 | + if ( wp_http_supports( array( 'ssl' ) ) ) { | |
| 288 | + $url = set_url_scheme( $url, 'https' ); | |
| 289 | + } | |
| 290 | + | |
| 291 | + $response = wp_remote_get( $url ); | |
| 292 | + | |
| 293 | + $body = trim( wp_remote_retrieve_body( $response ) ); | |
| 294 | + $body = json_decode( $body, true ); | |
| 295 | + | |
| 296 | + $return = time(); | |
| 297 | + if ( is_array( $body ) && isset( $body['files'] ) && is_array( $body['files'] ) ) { | |
| 298 | + $return = array_filter( | |
| 299 | + array_keys( $body['files'] ), | |
| 300 | + array( __CLASS__, 'is_js_or_css_file' ) | |
| 301 | + ); | |
| 302 | + } | |
| 303 | + | |
| 304 | + $cache[ $plugin ] = array(); | |
| 305 | + $cache[ $plugin ][ $version ] = $return; | |
| 306 | + Jetpack_Options::update_option( 'static_asset_cdn_files', $cache, true ); | |
| 307 | + | |
| 308 | + return $return; | |
| 309 | + } | |
| 310 | + | |
| 311 | + /** | |
| 312 | + * Checks a path whether it is a JS or CSS file. | |
| 313 | + * | |
| 314 | + * @param String $path file path. | |
| 315 | + * @return Boolean whether the file is a JS or CSS. | |
| 316 | + */ | |
| 317 | + public static function is_js_or_css_file( $path ) { | |
| 318 | + return ( ! str_contains( $path, '?' ) ) && in_array( substr( $path, -3 ), array( 'css', '.js' ), true ); | |
| 319 | + } | |
| 320 | + | |
| 321 | + /** | |
| 322 | + * Checks whether the version string indicates a production version. | |
| 323 | + * | |
| 324 | + * @param String $version the version string. | |
| 325 | + * @param Boolean $include_beta_and_rc whether to count beta and RC versions as production. | |
| 326 | + * @return Boolean | |
| 327 | + */ | |
| 328 | + public static function is_public_version( $version, $include_beta_and_rc = false ) { | |
| 329 | + if ( preg_match( '/^\d+(\.\d+)+$/', $version ) ) { | |
| 330 | + /** Example matches: `1`, `1.2`, `1.2.3`. */ | |
| 331 | + return true; | |
| 332 | + } elseif ( $include_beta_and_rc && preg_match( '/^\d+(\.\d+)+(-(beta|rc|pressable)\d?)$/i', $version ) ) { | |
| 333 | + /** Example matches: `1.2.3`, `1.2.3-beta`, `1.2.3-pressable`, `1.2.3-beta1`, `1.2.3-rc`, `1.2.3-rc2`. */ | |
| 334 | + return true; | |
| 335 | + } | |
| 336 | + // Unrecognized version. | |
| 337 | + return false; | |
| 338 | + } | |
| 339 | +} | |
| 340 | +/** | |
| 341 | + * Allow plugins to short-circuit the Asset CDN, even when the module is on. | |
| 342 | + * | |
| 343 | + * @module photon-cdn | |
| 344 | + * | |
| 345 | + * @since 6.7.0 | |
| 346 | + * | |
| 347 | + * @param false bool Should the Asset CDN be blocked? False by default. | |
| 348 | + */ | |
| 349 | +if ( true !== apply_filters( 'jetpack_force_disable_site_accelerator', false ) ) { | |
| 350 | + Jetpack_Photon_Static_Assets_CDN::go(); | |
| 351 | +} | |