Changeset 3203528
- Timestamp:
- 12/06/2024 10:34:20 AM (13 months ago)
- Location:
- nextgenthemes-jsdelivr-this
- Files:
-
- 2 added
- 4 edited
- 1 copied
-
tags/1.2.1 (copied) (copied from nextgenthemes-jsdelivr-this/trunk)
-
tags/1.2.1/dialog.js (added)
-
tags/1.2.1/nextgenthemes-jsdelivr-this.php (modified) (13 diffs)
-
tags/1.2.1/readme.txt (modified) (4 diffs)
-
trunk/dialog.js (added)
-
trunk/nextgenthemes-jsdelivr-this.php (modified) (13 diffs)
-
trunk/readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
nextgenthemes-jsdelivr-this/tags/1.2.1/nextgenthemes-jsdelivr-this.php
r3203494 r3203528 2 2 /** 3 3 * @wordpress-plugin 4 * Plugin Name: NGTjsDelivr CDN4 * Plugin Name: Free jsDelivr CDN 5 5 * Plugin URI: https://nextgenthemes.com 6 * Description: Makes your site load all WP Core and plugin assets fromjsDelivr CDN7 * Version: 1. 1.16 * Description: Serves all available assets from free jsDelivr CDN 7 * Version: 1.2.1 8 8 * Requres PHP: 7.4 9 9 * Author: Nicolas Jonas … … 12 12 * License URI: http://www.gnu.org/licenses/gpl-3.0.html 13 13 */ 14 15 declare(strict_types = 1); 16 14 17 namespace Nextgenthemes\jsDelivrThis; 15 18 16 const VERSION = '1.1.1'; 17 18 add_filter( 'script_loader_src', __NAMESPACE__ . '\filter_script_loader_src', 10, 2 ); 19 add_filter( 'style_loader_src', __NAMESPACE__ . '\filter_style_loader_src', 10, 2 ); 20 21 add_filter( 22 'plugin_action_links_' . plugin_basename( __FILE__ ), 23 function ( array $links ) { 24 25 $links['donate'] = sprintf( 26 '<a href="https://nextgenthemes.com/donate/"><strong style="display: inline;">%s</strong></a>', 27 esc_html__( 'Donate', 'jsdelivr-this' ) 19 const VERSION = '1.2.1'; 20 21 add_action( 'plugins_loaded', __NAMESPACE__ . '\init' ); 22 23 function init(): void { 24 25 add_filter( 'wp_script_attributes', __NAMESPACE__ . '\filter_script_attributes', 10, 1 ); 26 add_filter( 'style_loader_tag', __NAMESPACE__ . '\filter_style_loader_tag', 10, 1 ); 27 28 add_action( 'admin_bar_menu', __NAMESPACE__ . '\add_item_to_admin_bar', 33 ); 29 30 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); 31 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); 32 33 add_action( 'admin_footer', __NAMESPACE__ . '\admin_bar_html' ); 34 add_action( 'wp_footer', __NAMESPACE__ . '\admin_bar_html' ); 35 36 add_action( 37 'init', 38 function (): void { 39 wp_register_script_module( 40 'ngt-jsdelivr-dialog', 41 plugins_url( 'dialog.js', __FILE__ ), 42 array(), 43 VERSION 44 ); 45 } 46 ); 47 48 add_filter( 49 'plugin_row_meta', 50 function ( array $links, string $file ): array { 51 52 if ( 'nextgenthemes-jsdelivr-this/nextgenthemes-jsdelivr-this.php' !== $file ) { 53 return $links; 54 } 55 $links[] = '<strong>' . arve_links() . '</strong>'; 56 57 return $links; 58 }, 59 10, 60 2 61 ); 62 63 add_filter( 64 'plugin_action_links_' . plugin_basename( __FILE__ ), 65 function ( array $links ) { 66 67 $links['donate'] = sprintf( 68 '<a href="https://nextgenthemes.com/donate/">%s</a>', 69 esc_html__( 'Donate', 'nextgenthemes-jsdelivr-this' ) 70 ); 71 72 return $links; 73 } 74 ); 75 } 76 77 function enqueue_assets(): void { 78 79 if ( is_admin_bar_showing() ) { 80 wp_enqueue_script_module( 'ngt-jsdelivr-dialog' ); 81 } 82 } 83 84 function add_item_to_admin_bar( object $admin_bar ): void { 85 // Add a new item to the admin bar 86 $admin_bar->add_node( 87 array( 88 'id' => 'ngt-jsdelivr', 89 'title' => ' ', 90 'href' => '#', 91 'parent' => 'top-secondary', 92 ) 93 ); 94 } 95 96 function arve_links(): string { 97 return wp_kses( 98 sprintf( 99 // translators: %1$s: link, %2$s: link 100 __( 'Level up your video embeds with <a href="%1$s">ARVE</a> or <a href="%2$s">ARVE Pro</a>', 'nextgenthemes-jsdelivr-this' ), 101 esc_url( 'https://wordpress.org/plugins/advanced-responsive-video-embedder/' ), 102 esc_url( 'https://nextgenthemes.com/plugins/arve-pro/' ) 103 ), 104 array( 'a' => array( 'href' => array() ) ) 105 ); 106 } 107 108 109 function admin_bar_html(): void { 110 111 if ( ! is_admin_bar_showing() ) { 112 return; 113 } 114 115 wp_enqueue_style( 'media-views' ); 116 117 ?> 118 <dialog class="ngt-jsdelivr-dialog"> 119 <div class="ngt-jsdelivr-dialog__header"> 120 <button type="button" class="media-modal-close"> 121 <span class="media-modal-icon"> 122 <span class="screen-reader-text">Close dialog</span> 123 </span> 124 </button> 125 </div> 126 <h3><?= esc_html__( 'jsDelivr CDN plugin by Nextgenthemes', 'nextgenthemes-jsdelivr-this' ); ?></h3> 127 <p> 128 <?php 129 esc_html_e( 130 'These are the assets loaded from jsDelivr CDN. Do not worry about old WP versions in the URLs, this is simply because the files were not modified. A sha384 hash check is used so you can be 100% sure the files loaded from jsDelivr are the exact same files that would be served from your server.', 131 'nextgenthemes-jsdelivr-this' 28 132 ); 29 30 return $links; 31 } 32 ); 33 34 function filter_script_loader_src( string $src, string $handle ): string { 35 return maybe_replace_src( 'script', $src, $handle ); 36 } 37 function filter_style_loader_src( string $src, string $handle ): string { 38 return maybe_replace_src( 'style', $src, $handle ); 39 } 40 41 function maybe_replace_src( string $type, string $src, string $handle ): string { 42 $src = detect_by_hash( $type, $src, $handle ); 43 $src = detect_plugin_asset( $type, $src, $handle ); 44 return $src; 45 } 46 133 ?> 134 </p> 135 <pre></pre> 136 <p> 137 <?php 138 echo wp_kses( 139 sprintf( 140 // translators: %1$s: link, %2$s: link 141 __( 'Level up your video embeds with <a href="%1$s">ARVE</a> or <a href="%2$s">ARVE Pro</a>', 'nextgenthemes-jsdelivr-this' ), 142 esc_url( 'https://wordpress.org/plugins/advanced-responsive-video-embedder/' ), 143 esc_url( 'https://nextgenthemes.com/plugins/arve-pro/' ) 144 ), 145 array( 'a' => array( 'href' => array() ) ) 146 ); 147 ?> 148 </p> 149 </dialog> 150 <style> 151 #wp-admin-bar-ngt-jsdelivr a { 152 cursor: pointer; 153 154 &:hover { 155 background-color: darkred !important; 156 } 157 } 158 .ngt-jsdelivr-dialog { 159 --dialog-padding: 1.2rem; 160 161 border: none; 162 border-radius: 2px; 163 box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 164 padding: 0 var(--dialog-padding); 165 width: 100dvw; 166 max-width: 50rem; 167 font-size: 1rem; 168 &::backdrop { 169 /* Style the backdrop */ 170 background-color: rgba(0, 0, 0, .9); 171 } 172 pre { 173 font-size: 14px; 174 overflow-x: auto; 175 } 176 } 177 178 .ngt-jsdelivr-dialog__header { 179 position: relative; 180 181 > button { 182 --btn-bg: oklch(0.91 0.01 281.07); 183 184 position: absolute; 185 top: 0; 186 right: calc(var(--dialog-padding) * -1); 187 border-radius: 0; 188 background: var(--btn-bg); 189 border-width: 0; 190 191 &:hover { 192 background-color: oklch(from var(--btn-bg) calc(l - 0.1) c h); 193 } 194 } 195 } 196 </style> 197 <?php 198 } 199 200 function filter_script_attributes( array $attributes ): array { 201 202 $by_hash = detect_by_hash( $attributes['src'] ); 203 204 if ( $by_hash ) { 205 $attributes['src'] = $by_hash['src']; 206 $attributes['integrity'] = $by_hash['integrity']; 207 $attributes['crossorigin'] = 'anonymous'; 208 209 // we already got what we wanted, so exit early 210 return $attributes; 211 } 212 213 $by_plugin = detect_plugin_asset( $attributes['src'], 'js' ); 214 215 if ( $by_plugin ) { 216 $attributes['src'] = $by_plugin['src']; 217 $attributes['integrity'] = $by_plugin['integrity']; 218 $attributes['crossorigin'] = 'anonymous'; 219 } 220 221 return $attributes; 222 } 223 224 function filter_style_loader_tag( string $html ): string { 225 226 $p = new \WP_HTML_Tag_Processor( $html ); 227 228 // we may have multiple links here, like with rel="preload" and regular 229 while ( $p->next_tag( 'link' ) ) { 230 231 $href = $p->get_attribute( 'href' ); 232 233 if ( ! $href ) { 234 continue; 235 } 236 237 $by_hash = detect_by_hash( $href ); 238 239 if ( $by_hash ) { 240 $p->set_attribute( 'href', $by_hash['src'] ); 241 $p->set_attribute( 'integrity', $by_hash['integrity'] ); 242 $p->set_attribute( 'crossorigin', 'anonymous' ); 243 244 // we already got what we wanted, so exit early 245 return $p->get_updated_html(); 246 } 247 248 $by_plugin = detect_plugin_asset( $href, 'css' ); 249 250 if ( $by_plugin ) { 251 $p->set_attribute( 'href', $by_plugin['src'] ); 252 $p->set_attribute( 'integrity', $by_plugin['integrity'] ); 253 $p->set_attribute( 'crossorigin', 'anonymous' ); 254 } 255 } 256 257 return $html; 258 } 259 260 /** 261 * Checks for a active plugin file based on a slug. 262 * 263 * @param string $plugin_slug The plugin slug to search for. 264 * 265 * @return string|null The path to the main plugin file if found, null otherwise. 266 */ 47 267 function get_plugin_dir_file( string $plugin_slug ): ?string { 48 268 … … 63 283 } 64 284 65 function detect_plugin_asset( string $type, string $src, string $handle ): string { 285 /** 286 * Detects if file can be served from CDN 287 * 288 * Given a <link href="..."> or <script src="..."> it detects CDN files 289 * 290 * Plugins hosted on wp.org need some trickery by this plugin as the jsDelivr API does not detect them by hash. 291 * #1 For wp.org assets the src URL most have `/plugins/plugin-slug/` in them and end with `.js` or `.css` (excluding cash busting `?ver=1.2.3`). 292 * #2 wp.org assets need to have its current version published as a tag on the wp.org plugins SVN, `trunk` will not work. 293 * 294 * @param string $src The src to detect. 295 * @param string $extension The extension of the file (css or js). 296 * 297 * @return array|null The array contains 'src' and 'integrity' if file and hash can be detected on the server and the file exists on the CDN. 298 */ 299 function detect_plugin_asset( string $src, string $extension ): ?array { 66 300 67 301 if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) { 68 302 return $src; 69 303 } 70 $ext = ( 'style' === $type ) ? 'css' : 'js'; 71 72 preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$ext)#", $src, $matches ); 304 305 preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$extension)#", $src, $matches ); 73 306 74 307 if ( ! empty( $matches['plugin_slug'] ) ) { … … 77 310 78 311 if ( empty( $plugin_dir_file ) ) { 79 return $src; 80 } 81 82 static $ran_already = false; 83 $plugin_ver = get_plugin_version( $plugin_dir_file ); 84 $cdn_file = "https://cdn.jsdelivr.net/wp/{$matches['plugin_slug']}/tags/$plugin_ver/{$matches['path']}"; 85 $transient_name = 'ngt_jsdelivr_this_' . $cdn_file; 86 $data = get_transient( $transient_name ); 87 88 if ( false === $data && ! $ran_already ) { 89 90 $opts['http']['timeout'] = 2; 91 92 $ran_already = true; 312 return null; 313 } 314 315 $plugin_ver = get_plugin_version( $plugin_dir_file ); 316 $cdn_file = 'https://cdn.jsdelivr.net/wp/' . $matches['plugin_slug'] . '/tags/' . $plugin_ver . '/' . $matches['path']; 317 $transient_name = shorten_transient_name( 'ngt-jsd_' . $cdn_file ); 318 319 $data = get_transient( $transient_name ); 320 321 if ( false === $data && ! call_limit() ) { 322 93 323 $data = new \stdClass(); 94 $file_headers = ngt_headers( $cdn_file);95 96 if ( ! empty( $file_headers[0] ) && 'HTTP/1.1 200 OK' === $file_headers[0]) {324 $file_headers = remote_get_head($cdn_file, [ 'timeout' => 2 ]); 325 326 if ( ! is_wp_error( $file_headers ) ) { 97 327 $data->file_exists = true; 98 $path = path_from_url( $src ); 99 100 if ( $path ) { 101 $data->integrity = gen_integrity( file_get_contents( $path ) ); 102 } 328 $data->integrity = integrity_for_src( $src ); 103 329 } 104 330 … … 108 334 109 335 if ( ! empty( $data->file_exists ) && ! empty( $data->integrity ) ) { 110 $src = $cdn_file; 111 add_integrity_to_asset( $type, $handle, $data->integrity ); 112 } 113 114 return $src; 115 } 116 117 /** 118 * Retrieves headers for the given URL. 119 * 120 * @param string $url The URL for which to retrieve headers. 121 * @return array|false Returns an array of headers on success or FALSE on failure. 122 */ 123 function ngt_headers( string $url ) { 124 125 $opts['http']['timeout'] = 2; 126 127 $context = stream_context_create( $opts ); 128 return @get_headers( $url, 0, $context ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 129 } 130 131 /** 132 * Adds integrity and crossorigin attributes to assets based on type. 133 * 134 * @param string $type The type of the asset ('script' or 'style'). 135 * @param string $handle The handle of the asset. 136 * @param string $integrity The integrity value to be added. 137 */ 138 function add_integrity_to_asset( string $type, string $handle, string $integrity ): void { 139 140 if ( 'script' === $type ) { 141 add_filter( 142 'wp_script_attributes', 143 function ( array $attr ) use ( $handle, $integrity ) { 144 145 if ( ! empty( $attr['src'] ) && 146 ! empty( $attr['id'] ) && 147 $attr['id'] === $handle . '-js' 148 ) { 149 $attr['integrity'] = $integrity; 150 $attr['crossorigin'] = 'anonymous'; 151 } 152 153 return $attr; 154 } 155 ); 156 } else { 157 add_filter( 158 'style_loader_tag', 159 function ( $html, $fn_handle ) use ( $handle, $integrity ) { 160 161 if ( $fn_handle === $handle ) { 162 163 $p = new \WP_HTML_Tag_Processor( $html ); 164 165 if ( $p->next_tag( 'link' ) && $p->get_attribute( 'href' ) ) { 166 167 $p->set_attribute( 'integrity', $integrity ); 168 $p->set_attribute( 'crossorigin', 'anonymous' ); 169 $html = $p->get_updated_html(); 170 } 171 } 172 173 return $html; 174 }, 175 10, 176 2 177 ); 178 } 179 } 180 181 function get_jsdelivr_hash_api_data( string $file_path, string $handle, string $src ): ?object { 182 183 static $ran_already = false; 184 $transient_name = "ngt_jsdelivr_this_{$handle}_{$src}_wp{$GLOBALS['wp_version']}"; 185 $result = get_transient( $transient_name ); 186 187 if ( false === $result && ! $ran_already ) { 188 189 $ran_already = true; 336 return [ 337 'src' => $cdn_file, 338 'integrity' => $data->integrity, 339 ]; 340 } 341 342 return null; 343 } 344 345 function integrity_for_src( string $src ): ?string { 346 $path = path_from_url( $src ); 347 348 if ( $path ) { 349 $file_content = file_get_contents( $path ); 350 351 if ( ! $file_content ) { 352 wp_trigger_error( __FUNCTION__, 'Could not read file: ' . $path ); 353 } else { 354 return gen_integrity( $file_content ); 355 } 356 } 357 358 return null; 359 } 360 361 function get_jsdelivr_hash_api_data( string $file_path, string $src ): ?object { 362 363 $transient_name = shorten_transient_name( 'ngt-jsd_' . $src); 364 $result = get_transient( $transient_name ); 365 366 if ( false === $result && ! call_limit() ) { 367 190 368 $result = new \stdClass(); 191 369 $file_content = file_get_contents( $file_path ); … … 194 372 $sha256 = hash( 'sha256', $file_content ); 195 373 $data = wp_safe_remote_get( 196 "https://data.jsdelivr.com/v1/lookup/hash/$sha256",374 'https://data.jsdelivr.com/v1/lookup/hash/' . $sha256, 197 375 array( 198 376 'user-agent' => 'https://nextgenthemes.com/plugins/jsdelivr-this', … … 202 380 203 381 if ( ! is_wp_error( $data ) ) { 204 $result = (object) json_decode( wp_remote_retrieve_body( $data ) ); 382 383 $body = wp_remote_retrieve_body( $data ); 384 385 if ( '' === $body ) { 386 wp_trigger_error( __FUNCTION__, 'Empty body' ); 387 } else { 388 389 try { 390 $result = (object) json_decode( $body, false, 5, JSON_THROW_ON_ERROR ); 391 } catch ( \Exception $e ) { 392 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 393 } 394 } 395 205 396 $result->integrity = gen_integrity( $file_content ); 206 397 } … … 211 402 } 212 403 404 // So we can used nulled return type on php 7.4. Union types require 8.0 213 405 if ( false === $result ) { 214 406 $result = null; … … 218 410 } 219 411 220 function detect_by_hash( string $ type, string $src, string $handle ): string{412 function detect_by_hash( string $src ): ?array { 221 413 222 414 if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) { … … 227 419 228 420 if ( $path ) { 229 $data = get_jsdelivr_hash_api_data( $path, $ handle, $src );421 $data = get_jsdelivr_hash_api_data( $path, $src ); 230 422 } 231 423 … … 241 433 $data->version . $data->file 242 434 ); 243 add_integrity_to_asset( $type, $handle, $data->integrity ); 244 } 245 246 return $src; 435 436 return [ 437 'src' => $src, 438 'integrity' => $data->integrity, 439 ]; 440 } 441 442 return null; 247 443 } 248 444 … … 273 469 } 274 470 471 /** 472 * Retrieves the file path for a given URL, relative to the WordPress root directory. 473 * 474 * First checks if the file exists in the WordPress root directory, and if not, then 475 * checks the parent directory of the WordPress root directory. 476 * 477 * @param string $url The URL to retrieve the file path for. 478 * @return string|null The file path if it exists, or null otherwise. 479 */ 275 480 function path_from_url( string $url ): ?string { 276 481 $parsed_url = wp_parse_url( $url ); … … 291 496 return $plugin_data['Version']; 292 497 } 498 499 /** 500 * @return mixed|WP_Error 501 */ 502 function remote_get_head( string $url, array $args = array() ) { 503 504 $response = wp_safe_remote_head( $url, $args ); 505 506 if ( is_wp_error( $response ) ) { 507 return $response; 508 } 509 510 $response_code = wp_remote_retrieve_response_code( $response ); 511 512 if ( 200 !== $response_code ) { 513 514 return new \WP_Error( 515 $response_code, 516 sprintf( 517 // Translators: 1 URL 2 HTTP response code. 518 __( 'url: %1$s Status code 200 expected but was %2$s.', 'advanced-responsive-video-embedder' ), 519 $url, 520 $response_code 521 ) 522 ); 523 } 524 525 return $response; 526 } 527 528 function shorten_transient_name( string $transient_name ): string { 529 530 $transient_name = str_replace( 'https://', '', $transient_name ); 531 532 if ( strlen($transient_name) > 172 ) { 533 $transient_name = preg_replace( '/[^a-zA-Z0-9_]/', '', $transient_name ); 534 } 535 536 if ( strlen($transient_name) > 172 ) { 537 $transient_name = substr($transient_name, 0, 107) . '_' . hash( 'sha256', $transient_name ); // 107 + 1 + 64 538 } 539 540 return $transient_name; 541 } 542 543 /** 544 * Limit the number of remote requests to jsDelivr in a short timespan. 545 * In THEORY after the plugin is activated having this unlimited could 546 * take the first php page generation of a page a long time, so we limit 547 * it to 2 (this) x 2 (wp_safe_remote_get timeout) seconds at the time 548 * of writing. 549 * 550 * @return bool True if the limit is reached, false otherwise. 551 */ 552 function call_limit(): bool { 553 554 static $limit = 2; 555 556 if ( 0 === $limit ) { 557 return true; 558 } 559 560 --$limit; 561 562 return false; 563 } -
nextgenthemes-jsdelivr-this/tags/1.2.1/readme.txt
r3203494 r3203528 1 1 2 === NGT jsDelivr CDN === 2 3 Contributors: nico23 … … 6 7 Requires PHP: 7.4 7 8 Tested up to: 6.5.3 8 Stable tag: 1. 1.19 Stable tag: 1.2.1 9 10 License: GPL 3.0 10 11 License URI: http://www.gnu.org/licenses/gpl-3.0.html 11 12 12 Free CDN for WordPress Core and Plugin assets.13 Free CDN for for all assets from wordpress.org Github and NPM. 13 14 14 15 == Changelog == 15 16 16 = 2024-12-06 1.1.1 = 17 * 1.2 was broken. Revert release. 17 = 2024-12-06 1.2.1 = 18 * Fix: Code mistake caused `integrity` attribute to be wrong, plugin files would get blocked. 19 20 = 2024-12-04 1.2.0 = 21 * New: A info dialog was added that is only loaded when the admin bar is visible. 22 * New/Fix: Support for script modules. 23 * Improved: Shorten potentially too long transient names. 24 * Improved: Replaced `get_headers` with `wp_safe_remote_head`. WP coding standards and more efficient. 25 * Improved: Simplified and improved the code. 18 26 19 27 = 2024-05-14 1.1.0 = … … 23 31 * Run only once per page-load. 24 32 * Better function names and some useful comments. 25 * Send this plugins url as user-agent to jsDelivr knows how it s used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer sendsyour site URL (I really do not like that)33 * Send this plugins url as user-agent to jsDelivr knows how it's used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer by default would send your site URL (I really do not like that) 26 34 27 35 = 2019-08-31 0.9.4 = … … 35 43 36 44 == Description == 37 It replaces all WP Core assets with versions hosted on jsDelivr.45 It replaces all assets with versions available on jsDelivr. No options, nothing to configure, just works. 38 46 39 The following conditions need to be met that plugins assets will be served from jsDeliver:47 The code needs to be openly hosted on NPM, Github or wordpress.org. 40 48 41 1. The plugin needs to be hosted on wp.org. Commercial plugins and plugins from elsewhere will not work because jsDelivr mirrors the wp.org plugin dir and nothing else. 42 1. The asset src URL most have `/plugins/plugin-slug/` in them and end with `.js` or `.css` (excluding cash busting `?ver=1.2.3`). 43 1. It needs to have its current version published as a tag on the wp.org plugins SVN. Some plugins may only push to /trunk/ (like my own at the time of writing) or do not have their latest version published as tags. 49 This plugin adds a little a invisible button on the admin bar on the top right, left of "Howdy, Name". You can click that and see the assets loaded from jsDelivr. 44 50 45 = Donations are really appreciated=51 = Support me = 46 52 47 It took me a lot of time to come up with this plugin and I had many iterations over various different approaches how to do this until I came up with this working solution that also does not need much code. I know the official plugin was abandoned years ago and I looked at complicated bloated code and did not even feel like learning what its doing and never looked at it again and started from scratch. [Please donate here](https://nextgenthemes.com/donate/). 53 It took me a lot of time to come up with this plugin and I had many iterations over various different approaches how to do this until I came up with this working solution that also does not need much code. I know the official plugin was abandoned years ago and I looked at complicated bloated code and did not even feel like learning what its doing and never looked at it again and started from scratch. 54 55 Please check out my commercial plugin and level up your video embeds with [ARVE Pro](https://nextgenthemes.com/plugins/arve-pro/) or [Donate here](https://nextgenthemes.com/donate/) -
nextgenthemes-jsdelivr-this/trunk/nextgenthemes-jsdelivr-this.php
r3203494 r3203528 2 2 /** 3 3 * @wordpress-plugin 4 * Plugin Name: NGTjsDelivr CDN4 * Plugin Name: Free jsDelivr CDN 5 5 * Plugin URI: https://nextgenthemes.com 6 * Description: Makes your site load all WP Core and plugin assets fromjsDelivr CDN7 * Version: 1. 1.16 * Description: Serves all available assets from free jsDelivr CDN 7 * Version: 1.2.1 8 8 * Requres PHP: 7.4 9 9 * Author: Nicolas Jonas … … 12 12 * License URI: http://www.gnu.org/licenses/gpl-3.0.html 13 13 */ 14 15 declare(strict_types = 1); 16 14 17 namespace Nextgenthemes\jsDelivrThis; 15 18 16 const VERSION = '1.1.1'; 17 18 add_filter( 'script_loader_src', __NAMESPACE__ . '\filter_script_loader_src', 10, 2 ); 19 add_filter( 'style_loader_src', __NAMESPACE__ . '\filter_style_loader_src', 10, 2 ); 20 21 add_filter( 22 'plugin_action_links_' . plugin_basename( __FILE__ ), 23 function ( array $links ) { 24 25 $links['donate'] = sprintf( 26 '<a href="https://nextgenthemes.com/donate/"><strong style="display: inline;">%s</strong></a>', 27 esc_html__( 'Donate', 'jsdelivr-this' ) 19 const VERSION = '1.2.1'; 20 21 add_action( 'plugins_loaded', __NAMESPACE__ . '\init' ); 22 23 function init(): void { 24 25 add_filter( 'wp_script_attributes', __NAMESPACE__ . '\filter_script_attributes', 10, 1 ); 26 add_filter( 'style_loader_tag', __NAMESPACE__ . '\filter_style_loader_tag', 10, 1 ); 27 28 add_action( 'admin_bar_menu', __NAMESPACE__ . '\add_item_to_admin_bar', 33 ); 29 30 add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); 31 add_action( 'admin_enqueue_scripts', __NAMESPACE__ . '\enqueue_assets' ); 32 33 add_action( 'admin_footer', __NAMESPACE__ . '\admin_bar_html' ); 34 add_action( 'wp_footer', __NAMESPACE__ . '\admin_bar_html' ); 35 36 add_action( 37 'init', 38 function (): void { 39 wp_register_script_module( 40 'ngt-jsdelivr-dialog', 41 plugins_url( 'dialog.js', __FILE__ ), 42 array(), 43 VERSION 44 ); 45 } 46 ); 47 48 add_filter( 49 'plugin_row_meta', 50 function ( array $links, string $file ): array { 51 52 if ( 'nextgenthemes-jsdelivr-this/nextgenthemes-jsdelivr-this.php' !== $file ) { 53 return $links; 54 } 55 $links[] = '<strong>' . arve_links() . '</strong>'; 56 57 return $links; 58 }, 59 10, 60 2 61 ); 62 63 add_filter( 64 'plugin_action_links_' . plugin_basename( __FILE__ ), 65 function ( array $links ) { 66 67 $links['donate'] = sprintf( 68 '<a href="https://nextgenthemes.com/donate/">%s</a>', 69 esc_html__( 'Donate', 'nextgenthemes-jsdelivr-this' ) 70 ); 71 72 return $links; 73 } 74 ); 75 } 76 77 function enqueue_assets(): void { 78 79 if ( is_admin_bar_showing() ) { 80 wp_enqueue_script_module( 'ngt-jsdelivr-dialog' ); 81 } 82 } 83 84 function add_item_to_admin_bar( object $admin_bar ): void { 85 // Add a new item to the admin bar 86 $admin_bar->add_node( 87 array( 88 'id' => 'ngt-jsdelivr', 89 'title' => ' ', 90 'href' => '#', 91 'parent' => 'top-secondary', 92 ) 93 ); 94 } 95 96 function arve_links(): string { 97 return wp_kses( 98 sprintf( 99 // translators: %1$s: link, %2$s: link 100 __( 'Level up your video embeds with <a href="%1$s">ARVE</a> or <a href="%2$s">ARVE Pro</a>', 'nextgenthemes-jsdelivr-this' ), 101 esc_url( 'https://wordpress.org/plugins/advanced-responsive-video-embedder/' ), 102 esc_url( 'https://nextgenthemes.com/plugins/arve-pro/' ) 103 ), 104 array( 'a' => array( 'href' => array() ) ) 105 ); 106 } 107 108 109 function admin_bar_html(): void { 110 111 if ( ! is_admin_bar_showing() ) { 112 return; 113 } 114 115 wp_enqueue_style( 'media-views' ); 116 117 ?> 118 <dialog class="ngt-jsdelivr-dialog"> 119 <div class="ngt-jsdelivr-dialog__header"> 120 <button type="button" class="media-modal-close"> 121 <span class="media-modal-icon"> 122 <span class="screen-reader-text">Close dialog</span> 123 </span> 124 </button> 125 </div> 126 <h3><?= esc_html__( 'jsDelivr CDN plugin by Nextgenthemes', 'nextgenthemes-jsdelivr-this' ); ?></h3> 127 <p> 128 <?php 129 esc_html_e( 130 'These are the assets loaded from jsDelivr CDN. Do not worry about old WP versions in the URLs, this is simply because the files were not modified. A sha384 hash check is used so you can be 100% sure the files loaded from jsDelivr are the exact same files that would be served from your server.', 131 'nextgenthemes-jsdelivr-this' 28 132 ); 29 30 return $links; 31 } 32 ); 33 34 function filter_script_loader_src( string $src, string $handle ): string { 35 return maybe_replace_src( 'script', $src, $handle ); 36 } 37 function filter_style_loader_src( string $src, string $handle ): string { 38 return maybe_replace_src( 'style', $src, $handle ); 39 } 40 41 function maybe_replace_src( string $type, string $src, string $handle ): string { 42 $src = detect_by_hash( $type, $src, $handle ); 43 $src = detect_plugin_asset( $type, $src, $handle ); 44 return $src; 45 } 46 133 ?> 134 </p> 135 <pre></pre> 136 <p> 137 <?php 138 echo wp_kses( 139 sprintf( 140 // translators: %1$s: link, %2$s: link 141 __( 'Level up your video embeds with <a href="%1$s">ARVE</a> or <a href="%2$s">ARVE Pro</a>', 'nextgenthemes-jsdelivr-this' ), 142 esc_url( 'https://wordpress.org/plugins/advanced-responsive-video-embedder/' ), 143 esc_url( 'https://nextgenthemes.com/plugins/arve-pro/' ) 144 ), 145 array( 'a' => array( 'href' => array() ) ) 146 ); 147 ?> 148 </p> 149 </dialog> 150 <style> 151 #wp-admin-bar-ngt-jsdelivr a { 152 cursor: pointer; 153 154 &:hover { 155 background-color: darkred !important; 156 } 157 } 158 .ngt-jsdelivr-dialog { 159 --dialog-padding: 1.2rem; 160 161 border: none; 162 border-radius: 2px; 163 box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); 164 padding: 0 var(--dialog-padding); 165 width: 100dvw; 166 max-width: 50rem; 167 font-size: 1rem; 168 &::backdrop { 169 /* Style the backdrop */ 170 background-color: rgba(0, 0, 0, .9); 171 } 172 pre { 173 font-size: 14px; 174 overflow-x: auto; 175 } 176 } 177 178 .ngt-jsdelivr-dialog__header { 179 position: relative; 180 181 > button { 182 --btn-bg: oklch(0.91 0.01 281.07); 183 184 position: absolute; 185 top: 0; 186 right: calc(var(--dialog-padding) * -1); 187 border-radius: 0; 188 background: var(--btn-bg); 189 border-width: 0; 190 191 &:hover { 192 background-color: oklch(from var(--btn-bg) calc(l - 0.1) c h); 193 } 194 } 195 } 196 </style> 197 <?php 198 } 199 200 function filter_script_attributes( array $attributes ): array { 201 202 $by_hash = detect_by_hash( $attributes['src'] ); 203 204 if ( $by_hash ) { 205 $attributes['src'] = $by_hash['src']; 206 $attributes['integrity'] = $by_hash['integrity']; 207 $attributes['crossorigin'] = 'anonymous'; 208 209 // we already got what we wanted, so exit early 210 return $attributes; 211 } 212 213 $by_plugin = detect_plugin_asset( $attributes['src'], 'js' ); 214 215 if ( $by_plugin ) { 216 $attributes['src'] = $by_plugin['src']; 217 $attributes['integrity'] = $by_plugin['integrity']; 218 $attributes['crossorigin'] = 'anonymous'; 219 } 220 221 return $attributes; 222 } 223 224 function filter_style_loader_tag( string $html ): string { 225 226 $p = new \WP_HTML_Tag_Processor( $html ); 227 228 // we may have multiple links here, like with rel="preload" and regular 229 while ( $p->next_tag( 'link' ) ) { 230 231 $href = $p->get_attribute( 'href' ); 232 233 if ( ! $href ) { 234 continue; 235 } 236 237 $by_hash = detect_by_hash( $href ); 238 239 if ( $by_hash ) { 240 $p->set_attribute( 'href', $by_hash['src'] ); 241 $p->set_attribute( 'integrity', $by_hash['integrity'] ); 242 $p->set_attribute( 'crossorigin', 'anonymous' ); 243 244 // we already got what we wanted, so exit early 245 return $p->get_updated_html(); 246 } 247 248 $by_plugin = detect_plugin_asset( $href, 'css' ); 249 250 if ( $by_plugin ) { 251 $p->set_attribute( 'href', $by_plugin['src'] ); 252 $p->set_attribute( 'integrity', $by_plugin['integrity'] ); 253 $p->set_attribute( 'crossorigin', 'anonymous' ); 254 } 255 } 256 257 return $html; 258 } 259 260 /** 261 * Checks for a active plugin file based on a slug. 262 * 263 * @param string $plugin_slug The plugin slug to search for. 264 * 265 * @return string|null The path to the main plugin file if found, null otherwise. 266 */ 47 267 function get_plugin_dir_file( string $plugin_slug ): ?string { 48 268 … … 63 283 } 64 284 65 function detect_plugin_asset( string $type, string $src, string $handle ): string { 285 /** 286 * Detects if file can be served from CDN 287 * 288 * Given a <link href="..."> or <script src="..."> it detects CDN files 289 * 290 * Plugins hosted on wp.org need some trickery by this plugin as the jsDelivr API does not detect them by hash. 291 * #1 For wp.org assets the src URL most have `/plugins/plugin-slug/` in them and end with `.js` or `.css` (excluding cash busting `?ver=1.2.3`). 292 * #2 wp.org assets need to have its current version published as a tag on the wp.org plugins SVN, `trunk` will not work. 293 * 294 * @param string $src The src to detect. 295 * @param string $extension The extension of the file (css or js). 296 * 297 * @return array|null The array contains 'src' and 'integrity' if file and hash can be detected on the server and the file exists on the CDN. 298 */ 299 function detect_plugin_asset( string $src, string $extension ): ?array { 66 300 67 301 if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) { 68 302 return $src; 69 303 } 70 $ext = ( 'style' === $type ) ? 'css' : 'js'; 71 72 preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$ext)#", $src, $matches ); 304 305 preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$extension)#", $src, $matches ); 73 306 74 307 if ( ! empty( $matches['plugin_slug'] ) ) { … … 77 310 78 311 if ( empty( $plugin_dir_file ) ) { 79 return $src; 80 } 81 82 static $ran_already = false; 83 $plugin_ver = get_plugin_version( $plugin_dir_file ); 84 $cdn_file = "https://cdn.jsdelivr.net/wp/{$matches['plugin_slug']}/tags/$plugin_ver/{$matches['path']}"; 85 $transient_name = 'ngt_jsdelivr_this_' . $cdn_file; 86 $data = get_transient( $transient_name ); 87 88 if ( false === $data && ! $ran_already ) { 89 90 $opts['http']['timeout'] = 2; 91 92 $ran_already = true; 312 return null; 313 } 314 315 $plugin_ver = get_plugin_version( $plugin_dir_file ); 316 $cdn_file = 'https://cdn.jsdelivr.net/wp/' . $matches['plugin_slug'] . '/tags/' . $plugin_ver . '/' . $matches['path']; 317 $transient_name = shorten_transient_name( 'ngt-jsd_' . $cdn_file ); 318 319 $data = get_transient( $transient_name ); 320 321 if ( false === $data && ! call_limit() ) { 322 93 323 $data = new \stdClass(); 94 $file_headers = ngt_headers( $cdn_file);95 96 if ( ! empty( $file_headers[0] ) && 'HTTP/1.1 200 OK' === $file_headers[0]) {324 $file_headers = remote_get_head($cdn_file, [ 'timeout' => 2 ]); 325 326 if ( ! is_wp_error( $file_headers ) ) { 97 327 $data->file_exists = true; 98 $path = path_from_url( $src ); 99 100 if ( $path ) { 101 $data->integrity = gen_integrity( file_get_contents( $path ) ); 102 } 328 $data->integrity = integrity_for_src( $src ); 103 329 } 104 330 … … 108 334 109 335 if ( ! empty( $data->file_exists ) && ! empty( $data->integrity ) ) { 110 $src = $cdn_file; 111 add_integrity_to_asset( $type, $handle, $data->integrity ); 112 } 113 114 return $src; 115 } 116 117 /** 118 * Retrieves headers for the given URL. 119 * 120 * @param string $url The URL for which to retrieve headers. 121 * @return array|false Returns an array of headers on success or FALSE on failure. 122 */ 123 function ngt_headers( string $url ) { 124 125 $opts['http']['timeout'] = 2; 126 127 $context = stream_context_create( $opts ); 128 return @get_headers( $url, 0, $context ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 129 } 130 131 /** 132 * Adds integrity and crossorigin attributes to assets based on type. 133 * 134 * @param string $type The type of the asset ('script' or 'style'). 135 * @param string $handle The handle of the asset. 136 * @param string $integrity The integrity value to be added. 137 */ 138 function add_integrity_to_asset( string $type, string $handle, string $integrity ): void { 139 140 if ( 'script' === $type ) { 141 add_filter( 142 'wp_script_attributes', 143 function ( array $attr ) use ( $handle, $integrity ) { 144 145 if ( ! empty( $attr['src'] ) && 146 ! empty( $attr['id'] ) && 147 $attr['id'] === $handle . '-js' 148 ) { 149 $attr['integrity'] = $integrity; 150 $attr['crossorigin'] = 'anonymous'; 151 } 152 153 return $attr; 154 } 155 ); 156 } else { 157 add_filter( 158 'style_loader_tag', 159 function ( $html, $fn_handle ) use ( $handle, $integrity ) { 160 161 if ( $fn_handle === $handle ) { 162 163 $p = new \WP_HTML_Tag_Processor( $html ); 164 165 if ( $p->next_tag( 'link' ) && $p->get_attribute( 'href' ) ) { 166 167 $p->set_attribute( 'integrity', $integrity ); 168 $p->set_attribute( 'crossorigin', 'anonymous' ); 169 $html = $p->get_updated_html(); 170 } 171 } 172 173 return $html; 174 }, 175 10, 176 2 177 ); 178 } 179 } 180 181 function get_jsdelivr_hash_api_data( string $file_path, string $handle, string $src ): ?object { 182 183 static $ran_already = false; 184 $transient_name = "ngt_jsdelivr_this_{$handle}_{$src}_wp{$GLOBALS['wp_version']}"; 185 $result = get_transient( $transient_name ); 186 187 if ( false === $result && ! $ran_already ) { 188 189 $ran_already = true; 336 return [ 337 'src' => $cdn_file, 338 'integrity' => $data->integrity, 339 ]; 340 } 341 342 return null; 343 } 344 345 function integrity_for_src( string $src ): ?string { 346 $path = path_from_url( $src ); 347 348 if ( $path ) { 349 $file_content = file_get_contents( $path ); 350 351 if ( ! $file_content ) { 352 wp_trigger_error( __FUNCTION__, 'Could not read file: ' . $path ); 353 } else { 354 return gen_integrity( $file_content ); 355 } 356 } 357 358 return null; 359 } 360 361 function get_jsdelivr_hash_api_data( string $file_path, string $src ): ?object { 362 363 $transient_name = shorten_transient_name( 'ngt-jsd_' . $src); 364 $result = get_transient( $transient_name ); 365 366 if ( false === $result && ! call_limit() ) { 367 190 368 $result = new \stdClass(); 191 369 $file_content = file_get_contents( $file_path ); … … 194 372 $sha256 = hash( 'sha256', $file_content ); 195 373 $data = wp_safe_remote_get( 196 "https://data.jsdelivr.com/v1/lookup/hash/$sha256",374 'https://data.jsdelivr.com/v1/lookup/hash/' . $sha256, 197 375 array( 198 376 'user-agent' => 'https://nextgenthemes.com/plugins/jsdelivr-this', … … 202 380 203 381 if ( ! is_wp_error( $data ) ) { 204 $result = (object) json_decode( wp_remote_retrieve_body( $data ) ); 382 383 $body = wp_remote_retrieve_body( $data ); 384 385 if ( '' === $body ) { 386 wp_trigger_error( __FUNCTION__, 'Empty body' ); 387 } else { 388 389 try { 390 $result = (object) json_decode( $body, false, 5, JSON_THROW_ON_ERROR ); 391 } catch ( \Exception $e ) { 392 wp_trigger_error( __FUNCTION__, $e->getMessage() ); 393 } 394 } 395 205 396 $result->integrity = gen_integrity( $file_content ); 206 397 } … … 211 402 } 212 403 404 // So we can used nulled return type on php 7.4. Union types require 8.0 213 405 if ( false === $result ) { 214 406 $result = null; … … 218 410 } 219 411 220 function detect_by_hash( string $ type, string $src, string $handle ): string{412 function detect_by_hash( string $src ): ?array { 221 413 222 414 if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) { … … 227 419 228 420 if ( $path ) { 229 $data = get_jsdelivr_hash_api_data( $path, $ handle, $src );421 $data = get_jsdelivr_hash_api_data( $path, $src ); 230 422 } 231 423 … … 241 433 $data->version . $data->file 242 434 ); 243 add_integrity_to_asset( $type, $handle, $data->integrity ); 244 } 245 246 return $src; 435 436 return [ 437 'src' => $src, 438 'integrity' => $data->integrity, 439 ]; 440 } 441 442 return null; 247 443 } 248 444 … … 273 469 } 274 470 471 /** 472 * Retrieves the file path for a given URL, relative to the WordPress root directory. 473 * 474 * First checks if the file exists in the WordPress root directory, and if not, then 475 * checks the parent directory of the WordPress root directory. 476 * 477 * @param string $url The URL to retrieve the file path for. 478 * @return string|null The file path if it exists, or null otherwise. 479 */ 275 480 function path_from_url( string $url ): ?string { 276 481 $parsed_url = wp_parse_url( $url ); … … 291 496 return $plugin_data['Version']; 292 497 } 498 499 /** 500 * @return mixed|WP_Error 501 */ 502 function remote_get_head( string $url, array $args = array() ) { 503 504 $response = wp_safe_remote_head( $url, $args ); 505 506 if ( is_wp_error( $response ) ) { 507 return $response; 508 } 509 510 $response_code = wp_remote_retrieve_response_code( $response ); 511 512 if ( 200 !== $response_code ) { 513 514 return new \WP_Error( 515 $response_code, 516 sprintf( 517 // Translators: 1 URL 2 HTTP response code. 518 __( 'url: %1$s Status code 200 expected but was %2$s.', 'advanced-responsive-video-embedder' ), 519 $url, 520 $response_code 521 ) 522 ); 523 } 524 525 return $response; 526 } 527 528 function shorten_transient_name( string $transient_name ): string { 529 530 $transient_name = str_replace( 'https://', '', $transient_name ); 531 532 if ( strlen($transient_name) > 172 ) { 533 $transient_name = preg_replace( '/[^a-zA-Z0-9_]/', '', $transient_name ); 534 } 535 536 if ( strlen($transient_name) > 172 ) { 537 $transient_name = substr($transient_name, 0, 107) . '_' . hash( 'sha256', $transient_name ); // 107 + 1 + 64 538 } 539 540 return $transient_name; 541 } 542 543 /** 544 * Limit the number of remote requests to jsDelivr in a short timespan. 545 * In THEORY after the plugin is activated having this unlimited could 546 * take the first php page generation of a page a long time, so we limit 547 * it to 2 (this) x 2 (wp_safe_remote_get timeout) seconds at the time 548 * of writing. 549 * 550 * @return bool True if the limit is reached, false otherwise. 551 */ 552 function call_limit(): bool { 553 554 static $limit = 2; 555 556 if ( 0 === $limit ) { 557 return true; 558 } 559 560 --$limit; 561 562 return false; 563 } -
nextgenthemes-jsdelivr-this/trunk/readme.txt
r3203494 r3203528 1 1 2 === NGT jsDelivr CDN === 2 3 Contributors: nico23 … … 6 7 Requires PHP: 7.4 7 8 Tested up to: 6.5.3 8 Stable tag: 1. 1.19 Stable tag: 1.2.1 9 10 License: GPL 3.0 10 11 License URI: http://www.gnu.org/licenses/gpl-3.0.html 11 12 12 Free CDN for WordPress Core and Plugin assets.13 Free CDN for for all assets from wordpress.org Github and NPM. 13 14 14 15 == Changelog == 15 16 16 = 2024-12-06 1.1.1 = 17 * 1.2 was broken. Revert release. 17 = 2024-12-06 1.2.1 = 18 * Fix: Code mistake caused `integrity` attribute to be wrong, plugin files would get blocked. 19 20 = 2024-12-04 1.2.0 = 21 * New: A info dialog was added that is only loaded when the admin bar is visible. 22 * New/Fix: Support for script modules. 23 * Improved: Shorten potentially too long transient names. 24 * Improved: Replaced `get_headers` with `wp_safe_remote_head`. WP coding standards and more efficient. 25 * Improved: Simplified and improved the code. 18 26 19 27 = 2024-05-14 1.1.0 = … … 23 31 * Run only once per page-load. 24 32 * Better function names and some useful comments. 25 * Send this plugins url as user-agent to jsDelivr knows how it s used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer sendsyour site URL (I really do not like that)33 * Send this plugins url as user-agent to jsDelivr knows how it's used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer by default would send your site URL (I really do not like that) 26 34 27 35 = 2019-08-31 0.9.4 = … … 35 43 36 44 == Description == 37 It replaces all WP Core assets with versions hosted on jsDelivr.45 It replaces all assets with versions available on jsDelivr. No options, nothing to configure, just works. 38 46 39 The following conditions need to be met that plugins assets will be served from jsDeliver:47 The code needs to be openly hosted on NPM, Github or wordpress.org. 40 48 41 1. The plugin needs to be hosted on wp.org. Commercial plugins and plugins from elsewhere will not work because jsDelivr mirrors the wp.org plugin dir and nothing else. 42 1. The asset src URL most have `/plugins/plugin-slug/` in them and end with `.js` or `.css` (excluding cash busting `?ver=1.2.3`). 43 1. It needs to have its current version published as a tag on the wp.org plugins SVN. Some plugins may only push to /trunk/ (like my own at the time of writing) or do not have their latest version published as tags. 49 This plugin adds a little a invisible button on the admin bar on the top right, left of "Howdy, Name". You can click that and see the assets loaded from jsDelivr. 44 50 45 = Donations are really appreciated=51 = Support me = 46 52 47 It took me a lot of time to come up with this plugin and I had many iterations over various different approaches how to do this until I came up with this working solution that also does not need much code. I know the official plugin was abandoned years ago and I looked at complicated bloated code and did not even feel like learning what its doing and never looked at it again and started from scratch. [Please donate here](https://nextgenthemes.com/donate/). 53 It took me a lot of time to come up with this plugin and I had many iterations over various different approaches how to do this until I came up with this working solution that also does not need much code. I know the official plugin was abandoned years ago and I looked at complicated bloated code and did not even feel like learning what its doing and never looked at it again and started from scratch. 54 55 Please check out my commercial plugin and level up your video embeds with [ARVE Pro](https://nextgenthemes.com/plugins/arve-pro/) or [Donate here](https://nextgenthemes.com/donate/)
Note: See TracChangeset
for help on using the changeset viewer.