Plugin Directory

Changeset 3086793

Timestamp:
05/15/2024 03:06:12 AM (3 months ago)
Author:
nico23
Message:

Update plugin to version 1.1.0 with NextgenThemes WordPress Plugin Deploy

Location:
nextgenthemes-jsdelivr-this
Files:
2 deleted
6 edited
1 copied

Legend:

Unmodified
Added
Removed
  • nextgenthemes-jsdelivr-this/tags/1.1.0/composer.json

    r2148782 r3086793  
    33    "type": "wordpress-plugin",
    44    "require": {
    5         "php": ">=5.6"
    6     },
    7     "require-dev": {
    8         "php": ">=7.0",
    9         "phpunit/phpunit": "4.8.* || 5.7.*",
    10         "mobiledetect/mobiledetectlib": "*",
    11         "wp-coding-standards/wpcs": "*",
    12         "dealerdirect/phpcodesniffer-composer-installer": "*"
     5        "php": ">=7.4"
    136    },
    147    "license": "GPL-3.0",
  • nextgenthemes-jsdelivr-this/tags/1.1.0/nextgenthemes-jsdelivr-this.php

    r2155264 r3086793  
    55 * Plugin URI:        https://nextgenthemes.com
    66 * Description:       Makes your site load all WP Core and plugin assets from jsDelivr CDN
    7  * Version:           1.0.0
     7 * Version:           1.1.0
     8 * Requres PHP:       7.4
    89 * Author:            Nicolas Jonas
    910 * Author URI:        https://nextgenthemes.com/donate
     
    1112 * License URI:       http://www.gnu.org/licenses/gpl-3.0.html
    1213 */
    13 namespace Nextgenthemes\JSdelivrThis;
    14 
    15 const VERSION = '1.0.0';
    16 
    17 add_filter( 'script_loader_src', __NAMESPACE__ . '\\filter_script_loader_src', 10, 2 );
    18 add_filter( 'style_loader_src', __NAMESPACE__ . '\\filter_style_loader_src', 10, 2 );
    19 
    20 function filter_script_loader_src( $src, $handle ) {
    21     return maybe_replace_src( 'js', $src, $handle );
    22 };
    23 function filter_style_loader_src( $src, $handle ) {
    24     return maybe_replace_src( 'css', $src, $handle );
    25 };
    26 
    27 function maybe_replace_src( $ext, $src, $handle ) {
    28 
    29     static $ran_already = false;
    30 
    31     // We only run this once per page generation to avoid a bunch of API calls to slow the site down
    32     if ( ! $ran_already ) {
    33         $src = detect_by_hash( $ext, $src, $handle );
    34         $src = detect_plugin_asset( $ext, $src, $handle );
    35 
    36         $ran_already = true;
    37     }
    38 
     14namespace Nextgenthemes\jsDelivrThis;
     15
     16const VERSION = '1.1.0';
     17
     18add_filter( 'script_loader_src', __NAMESPACE__ . '\filter_script_loader_src', 10, 2 );
     19add_filter( 'style_loader_src', __NAMESPACE__ . '\filter_style_loader_src', 10, 2 );
     20
     21add_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' )
     28        );
     29
     30        return $links;
     31    }
     32);
     33
     34function filter_script_loader_src( string $src, string $handle ): string {
     35    return maybe_replace_src( 'script', $src, $handle );
     36}
     37function filter_style_loader_src( string $src, string $handle ): string {
     38    return maybe_replace_src( 'style', $src, $handle );
     39}
     40
     41function 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 );
    3944    return $src;
    4045}
    4146
    42 function get_plugin_dir_file( $plugin_slug ) {
     47function get_plugin_dir_file( {
    4348
    4449    $active_plugins = get_option( 'active_plugins' );
     
    5055    foreach ( $active_plugins as $key => $value ) {
    5156
    52         if ( starts_with( $value, $plugin_slug ) ) {
     57        if ( starts_with( $value, $plugin_slug ) ) {
    5358            return $value;
    5459        }
    5560    }
    5661
    57     return false;
    58 }
    59 
    60 function detect_plugin_asset( $ext, $src, $handle ) {
    61 
    62     if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
     62    return ;
     63}
     64
     65function detect_plugin_asset( {
     66
     67    if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
    6368        return $src;
    6469    }
     70
    6571
    6672    preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$ext)#", $src, $matches );
     
    7480    }
    7581
    76     $plugin_ver     = get_plugin_version( $plugin_dir_file );
    77     $cdn_file       = "https://cdn.jsdelivr.net/wp/{$matches['plugin_slug']}/tags/$plugin_ver/{$matches['path']}";
    78     $transient_name = "jsdelivr_this_{$cdn_file}_exists";
    79     $file_exists    = get_transient( $transient_name );
    80 
    81     if ( false === $file_exists ) {
    82 
    83         // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    84         $file_headers = @get_headers( $cdn_file );
    85 
    86         if ( 'HTTP/1.1 404 Not Found' === $file_headers[0] ) {
    87             $file_exists = 'no';
    88         } else {
    89             $file_exists = 'yes';
     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;
     93        $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] ) {
     97            $data->file_exists = true;
     98            $path              = path_from_url( $src );
     99
     100            if ( $path ) {
     101                $data->integrity = gen_integrity( file_get_contents( $path ) );
     102            }
    90103        }
    91104
    92105        // Random time between 24 and 48h to avoid calls getting made every pageload (if only one lonely visitor)
    93         set_transient( $transient_name, $file_exists, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
    94     }
    95 
    96     if ( 'yes' === $file_exists ) {
     106        set_transient( $transient_name, $, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
     107    }
     108
     109    if ( ) {
    97110        $src = $cdn_file;
     111
    98112    }
    99113
     
    101115}
    102116
    103 function detect_by_hash( $ext, $src, $handle ) {
    104 
    105     if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
     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 */
     123function 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 */
     138function 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
     181function 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;
     190        $result       = new \stdClass();
     191        $file_content = file_get_contents( $file_path );
     192
     193        if ( $file_content ) {
     194            $sha256 = hash( 'sha256', $file_content );
     195            $data   = wp_safe_remote_get(
     196                "https://data.jsdelivr.com/v1/lookup/hash/$sha256",
     197                array(
     198                    'user-agent' => 'https://nextgenthemes.com/plugins/jsdelivr-this',
     199                    'timeout'    => 2,
     200                )
     201            );
     202
     203            if ( ! is_wp_error( $data ) ) {
     204                $result            = (object) json_decode( wp_remote_retrieve_body( $data ) );
     205                $result->integrity = gen_integrity( $file_content );
     206            }
     207        }
     208
     209        // Random time between 24 and 48h to avoid calls getting made every pageload (if only one lonely visitor)
     210        set_transient( $transient_name, $result, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
     211    }
     212
     213    if ( false === $result ) {
     214        $result = null;
     215    }
     216
     217    return $result;
     218}
     219
     220function detect_by_hash( string $type, string $src, string $handle ): string {
     221
     222    if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
    106223        return $src;
    107224    }
    108225
    109     $parsed_url = wp_parse_url( $src );
     226    $path = path_from_url( $src );
     227
     228    if ( $path ) {
     229        $data = get_jsdelivr_hash_api_data( $path, $handle, $src );
     230    }
     231
     232    $ver         = get_url_arg( $src, 'ver' );
     233    $wp_gh_asset = ( ! empty( $data->type ) && 'gh' === $data->type && 'WordPress/WordPress' === $data->name );
     234    $ver_not_wp  = ( ! empty( $data->type ) && $ver && $GLOBALS['wp_version'] !== $ver );
     235
     236    if ( $wp_gh_asset || $ver_not_wp ) {
     237        $src = sprintf(
     238            'https://cdn.jsdelivr.net/%s/%s@%s',
     239            $data->type,
     240            $data->name,
     241            $data->version . $data->file
     242        );
     243        add_integrity_to_asset( $type, $handle, $data->integrity );
     244    }
     245
     246    return $src;
     247}
     248
     249/**
     250 * Retrieves the value of a specific query argument from the given URL.
     251 *
     252 * @param string $url The URL containing the query parameters.
     253 * @param string $arg The name of the query argument to retrieve.
     254 * @return string|null The value of the specified query argument, or null if it is not found.
     255 */
     256function get_url_arg( string $url, string $arg ): ?string {
     257
     258    $query_string = parse_url( $url, PHP_URL_QUERY );
     259
     260    if ( empty( $query_string ) || ! is_string( $query_string ) ) {
     261        return null;
     262    }
     263
     264    parse_str( $query_string, $query_args );
     265
     266    return $query_args[ $arg ] ?? null;
     267}
     268
     269function gen_integrity( string $input ): string {
     270    $hash        = hash( 'sha384', $input, true );
     271    $hash_base64 = base64_encode( $hash ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
     272    return "sha384-$hash_base64";
     273}
     274
     275function path_from_url( string $url ): ?string {
     276    $parsed_url = wp_parse_url( $url );
    110277    $file       = rtrim( ABSPATH, '/' ) . $parsed_url['path'];
    111278    $file_alt   = rtrim( dirname( ABSPATH ), '/' ) . $parsed_url['path'];
    112279
    113280    if ( is_file( $file ) ) {
    114         $data = get_jsdeliver_hash_api_data( $file );
    115     };
    116     if ( is_file( $file_alt ) ) {
    117         $data = get_jsdeliver_hash_api_data( $file_alt );
    118     };
    119 
    120     if ( isset( $data['type'] ) && 'gh' === $data['type'] ) {
    121         $src = "https://cdn.jsdelivr.net/{$data['type']}/{$data['name']}@{$data['version']}{$data['file']}";
    122     }
    123 
    124     return $src;
    125 }
    126 
    127 function get_jsdeliver_hash_api_data( $file_path ) {
    128 
    129     $transient_name = "jsdelivr_this_hashapi_wp{$GLOBALS['wp_version']}_$file_path";
    130     $result         = get_transient( $transient_name );
    131 
    132     if ( false === $result ) {
    133 
    134         // Local file, no need for wp_remote_get
    135         // phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    136         $result       = array();
    137         $file_content = file_get_contents( $file_path );
    138         // phpcs:enable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    139 
    140         if ( $file_content ) {
    141             $sha256 = hash( 'sha256', $file_content );
    142             $data   = wp_safe_remote_get( "https://data.jsdelivr.com/v1/lookup/hash/$sha256", array() );
    143 
    144             if ( ! is_wp_error( $data ) ) {
    145                 $result = (array) json_decode( wp_remote_retrieve_body( $data ), true );
    146             }
    147         }
    148 
    149         set_transient( $transient_name, $result, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
    150     }
    151 
    152     return $result;
    153 }
    154 
    155 function contains( $haystack, $needle ) {
    156     return strpos( $haystack, $needle ) !== false;
    157 }
    158 
    159 function starts_with( $haystack, $needle ) {
    160     return $haystack[0] === $needle[0] ? strncmp( $haystack, $needle, strlen( $needle ) ) === 0 : false;
    161 }
    162 
    163 function get_plugin_version( $plugin_file ) {
    164     $plugin_data = get_plugin_data( WP_PLUGIN_DIR . "/$plugin_file", false, false );
     281        return $file;
     282    } elseif ( is_file( $file_alt ) ) {
     283        return $file_alt;
     284    }
     285
     286    return null;
     287}
     288
     289function get_plugin_version( string $plugin_file ): string {
     290    $plugin_data = get_file_data( WP_PLUGIN_DIR . "/$plugin_file", array( 'Version' => 'Version' ), 'plugin' );
    165291    return $plugin_data['Version'];
    166292}
    167 
    168 /**
    169  * Parses the plugin contents to retrieve plugin's metadata.
    170  *
    171  * The metadata of the plugin's data searches for the following in the plugin's
    172  * header. All plugin data must be on its own line. For plugin description, it
    173  * must not have any newlines or only parts of the description will be displayed
    174  * and the same goes for the plugin data. The below is formatted for printing.
    175  *
    176  *     /*
    177  *     Plugin Name: Name of Plugin
    178  *     Plugin URI: Link to plugin information
    179  *     Description: Plugin Description
    180  *     Author: Plugin author's name
    181  *     Author URI: Link to the author's web site
    182  *     Version: Must be set in the plugin for WordPress 2.3+
    183  *     Text Domain: Optional. Unique identifier, should be same as the one used in
    184  *          load_plugin_textdomain()
    185  *     Domain Path: Optional. Only useful if the translations are located in a
    186  *          folder above the plugin's base path. For example, if .mo files are
    187  *          located in the locale folder then Domain Path will be "/locale/" and
    188  *          must have the first slash. Defaults to the base folder the plugin is
    189  *          located in.
    190  *     Network: Optional. Specify "Network: true" to require that a plugin is activated
    191  *          across all sites in an installation. This will prevent a plugin from being
    192  *          activated on a single site when Multisite is enabled.
    193  *      * / # Remove the space to close comment
    194  *
    195  * Some users have issues with opening large files and manipulating the contents
    196  * for want is usually the first 1kiB or 2kiB. This function stops pulling in
    197  * the plugin contents when it has all of the required plugin data.
    198  *
    199  * The first 8kiB of the file will be pulled in and if the plugin data is not
    200  * within that first 8kiB, then the plugin author should correct their plugin
    201  * and move the plugin data headers to the top.
    202  *
    203  * The plugin file is assumed to have permissions to allow for scripts to read
    204  * the file. This is not checked however and the file is only opened for
    205  * reading.
    206  *
    207  * @since 1.5.0
    208  *
    209  * @param string $plugin_file Path to the main plugin file.
    210  * @param bool   $markup      Optional. If the returned data should have HTML markup applied.
    211  *                            Default true.
    212  * @param bool   $translate   Optional. If the returned data should be translated. Default true.
    213  * @return array {
    214  *     Plugin data. Values will be empty if not supplied by the plugin.
    215  *
    216  *     @type string $Name        Name of the plugin. Should be unique.
    217  *     @type string $Title       Title of the plugin and link to the plugin's site (if set).
    218  *     @type string $Description Plugin description.
    219  *     @type string $Author      Author's name.
    220  *     @type string $AuthorURI   Author's website address (if set).
    221  *     @type string $Version     Plugin version.
    222  *     @type string $TextDomain  Plugin textdomain.
    223  *     @type string $DomainPath  Plugins relative directory path to .mo files.
    224  *     @type bool   $Network     Whether the plugin can only be activated network-wide.
    225  * }
    226  */
    227 function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
    228 
    229     $default_headers = array(
    230         'Name'        => 'Plugin Name',
    231         'PluginURI'   => 'Plugin URI',
    232         'Version'     => 'Version',
    233         'Description' => 'Description',
    234         'Author'      => 'Author',
    235         'AuthorURI'   => 'Author URI',
    236         'TextDomain'  => 'Text Domain',
    237         'DomainPath'  => 'Domain Path',
    238         'Network'     => 'Network',
    239         // Site Wide Only is deprecated in favor of Network.
    240         '_sitewide'   => 'Site Wide Only',
    241     );
    242 
    243     $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );
    244 
    245     // Site Wide Only is the old header for Network
    246     if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) {
    247         // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
    248         // phpcs:disable WordPress.WP.I18n.MissingArgDomain
    249         /* translators: 1: Site Wide Only: true, 2: Network: true */
    250         _deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), '<code>Site Wide Only: true</code>', '<code>Network: true</code>' ) );
    251         // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    252         // phpcs:enable WordPress.WP.I18n.MissingArgDomain
    253         $plugin_data['Network'] = $plugin_data['_sitewide'];
    254     }
    255     // phpcs:disable WordPress.PHP.StrictComparisons.LooseComparison
    256     $plugin_data['Network'] = ( 'true' == strtolower( $plugin_data['Network'] ) );
    257     // phpcs:enable WordPress.PHP.StrictComparisons.LooseComparison
    258     unset( $plugin_data['_sitewide'] );
    259 
    260     // If no text domain is defined fall back to the plugin slug.
    261     if ( ! $plugin_data['TextDomain'] ) {
    262         $plugin_slug = dirname( plugin_basename( $plugin_file ) );
    263         if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) {
    264             $plugin_data['TextDomain'] = $plugin_slug;
    265         }
    266     }
    267 
    268     if ( $markup || $translate ) {
    269         $plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate );
    270     } else {
    271         $plugin_data['Title']      = $plugin_data['Name'];
    272         $plugin_data['AuthorName'] = $plugin_data['Author'];
    273     }
    274 
    275     return $plugin_data;
    276 }
  • nextgenthemes-jsdelivr-this/tags/1.1.0/readme.txt

    r2155264 r3086793  
    11=== NGT jsDelivr CDN ===
    22Contributors: nico23
    3 Tags: CDN, JS, JavaScript, jQuery, Performance, minimalistic, minimal
     3Tags: CDN, JS, JavaScript, j
    44Donate link: https://nextgenthemes.com/donate
    5 Requires at least: 4.3.1
    6 Requires PHP: 5.6
    7 Tested up to: 5.2.2
    8 Stable tag: 1.0.0
     5Requires at least:
     6Requires PHP:
     7Tested up to:
     8Stable tag: 1..0
    99License: GPL 3.0
    1010License URI: http://www.gnu.org/licenses/gpl-3.0.html
     
    1414== Changelog ==
    1515
     16
     17
     18
    1619= 2019-09-07 1.0.0 =
    17 * Run only once per page-load, better function names and some useful comments.
     20* Run only once per page-load.
     21* Better function names and some useful comments.
     22* Send this plugins url as user-agent to jsDelivr knows how its used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer sends your site URL (I really do not like that)
    1823
    1924= 2019-08-31 0.9.4 =
  • nextgenthemes-jsdelivr-this/trunk/composer.json

    r2148782 r3086793  
    33    "type": "wordpress-plugin",
    44    "require": {
    5         "php": ">=5.6"
    6     },
    7     "require-dev": {
    8         "php": ">=7.0",
    9         "phpunit/phpunit": "4.8.* || 5.7.*",
    10         "mobiledetect/mobiledetectlib": "*",
    11         "wp-coding-standards/wpcs": "*",
    12         "dealerdirect/phpcodesniffer-composer-installer": "*"
     5        "php": ">=7.4"
    136    },
    147    "license": "GPL-3.0",
  • nextgenthemes-jsdelivr-this/trunk/nextgenthemes-jsdelivr-this.php

    r2155264 r3086793  
    55 * Plugin URI:        https://nextgenthemes.com
    66 * Description:       Makes your site load all WP Core and plugin assets from jsDelivr CDN
    7  * Version:           1.0.0
     7 * Version:           1.1.0
     8 * Requres PHP:       7.4
    89 * Author:            Nicolas Jonas
    910 * Author URI:        https://nextgenthemes.com/donate
     
    1112 * License URI:       http://www.gnu.org/licenses/gpl-3.0.html
    1213 */
    13 namespace Nextgenthemes\JSdelivrThis;
    14 
    15 const VERSION = '1.0.0';
    16 
    17 add_filter( 'script_loader_src', __NAMESPACE__ . '\\filter_script_loader_src', 10, 2 );
    18 add_filter( 'style_loader_src', __NAMESPACE__ . '\\filter_style_loader_src', 10, 2 );
    19 
    20 function filter_script_loader_src( $src, $handle ) {
    21     return maybe_replace_src( 'js', $src, $handle );
    22 };
    23 function filter_style_loader_src( $src, $handle ) {
    24     return maybe_replace_src( 'css', $src, $handle );
    25 };
    26 
    27 function maybe_replace_src( $ext, $src, $handle ) {
    28 
    29     static $ran_already = false;
    30 
    31     // We only run this once per page generation to avoid a bunch of API calls to slow the site down
    32     if ( ! $ran_already ) {
    33         $src = detect_by_hash( $ext, $src, $handle );
    34         $src = detect_plugin_asset( $ext, $src, $handle );
    35 
    36         $ran_already = true;
    37     }
    38 
     14namespace Nextgenthemes\jsDelivrThis;
     15
     16const VERSION = '1.1.0';
     17
     18add_filter( 'script_loader_src', __NAMESPACE__ . '\filter_script_loader_src', 10, 2 );
     19add_filter( 'style_loader_src', __NAMESPACE__ . '\filter_style_loader_src', 10, 2 );
     20
     21add_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' )
     28        );
     29
     30        return $links;
     31    }
     32);
     33
     34function filter_script_loader_src( string $src, string $handle ): string {
     35    return maybe_replace_src( 'script', $src, $handle );
     36}
     37function filter_style_loader_src( string $src, string $handle ): string {
     38    return maybe_replace_src( 'style', $src, $handle );
     39}
     40
     41function 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 );
    3944    return $src;
    4045}
    4146
    42 function get_plugin_dir_file( $plugin_slug ) {
     47function get_plugin_dir_file( {
    4348
    4449    $active_plugins = get_option( 'active_plugins' );
     
    5055    foreach ( $active_plugins as $key => $value ) {
    5156
    52         if ( starts_with( $value, $plugin_slug ) ) {
     57        if ( starts_with( $value, $plugin_slug ) ) {
    5358            return $value;
    5459        }
    5560    }
    5661
    57     return false;
    58 }
    59 
    60 function detect_plugin_asset( $ext, $src, $handle ) {
    61 
    62     if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
     62    return ;
     63}
     64
     65function detect_plugin_asset( {
     66
     67    if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
    6368        return $src;
    6469    }
     70
    6571
    6672    preg_match( "#/plugins/(?<plugin_slug>[^/]+)/(?<path>.*\.$ext)#", $src, $matches );
     
    7480    }
    7581
    76     $plugin_ver     = get_plugin_version( $plugin_dir_file );
    77     $cdn_file       = "https://cdn.jsdelivr.net/wp/{$matches['plugin_slug']}/tags/$plugin_ver/{$matches['path']}";
    78     $transient_name = "jsdelivr_this_{$cdn_file}_exists";
    79     $file_exists    = get_transient( $transient_name );
    80 
    81     if ( false === $file_exists ) {
    82 
    83         // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
    84         $file_headers = @get_headers( $cdn_file );
    85 
    86         if ( 'HTTP/1.1 404 Not Found' === $file_headers[0] ) {
    87             $file_exists = 'no';
    88         } else {
    89             $file_exists = 'yes';
     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;
     93        $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] ) {
     97            $data->file_exists = true;
     98            $path              = path_from_url( $src );
     99
     100            if ( $path ) {
     101                $data->integrity = gen_integrity( file_get_contents( $path ) );
     102            }
    90103        }
    91104
    92105        // Random time between 24 and 48h to avoid calls getting made every pageload (if only one lonely visitor)
    93         set_transient( $transient_name, $file_exists, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
    94     }
    95 
    96     if ( 'yes' === $file_exists ) {
     106        set_transient( $transient_name, $, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
     107    }
     108
     109    if ( ) {
    97110        $src = $cdn_file;
     111
    98112    }
    99113
     
    101115}
    102116
    103 function detect_by_hash( $ext, $src, $handle ) {
    104 
    105     if ( starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
     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 */
     123function 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 */
     138function 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
     181function 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;
     190        $result       = new \stdClass();
     191        $file_content = file_get_contents( $file_path );
     192
     193        if ( $file_content ) {
     194            $sha256 = hash( 'sha256', $file_content );
     195            $data   = wp_safe_remote_get(
     196                "https://data.jsdelivr.com/v1/lookup/hash/$sha256",
     197                array(
     198                    'user-agent' => 'https://nextgenthemes.com/plugins/jsdelivr-this',
     199                    'timeout'    => 2,
     200                )
     201            );
     202
     203            if ( ! is_wp_error( $data ) ) {
     204                $result            = (object) json_decode( wp_remote_retrieve_body( $data ) );
     205                $result->integrity = gen_integrity( $file_content );
     206            }
     207        }
     208
     209        // Random time between 24 and 48h to avoid calls getting made every pageload (if only one lonely visitor)
     210        set_transient( $transient_name, $result, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
     211    }
     212
     213    if ( false === $result ) {
     214        $result = null;
     215    }
     216
     217    return $result;
     218}
     219
     220function detect_by_hash( string $type, string $src, string $handle ): string {
     221
     222    if ( str_starts_with( $src, 'https://cdn.jsdelivr.net' ) ) {
    106223        return $src;
    107224    }
    108225
    109     $parsed_url = wp_parse_url( $src );
     226    $path = path_from_url( $src );
     227
     228    if ( $path ) {
     229        $data = get_jsdelivr_hash_api_data( $path, $handle, $src );
     230    }
     231
     232    $ver         = get_url_arg( $src, 'ver' );
     233    $wp_gh_asset = ( ! empty( $data->type ) && 'gh' === $data->type && 'WordPress/WordPress' === $data->name );
     234    $ver_not_wp  = ( ! empty( $data->type ) && $ver && $GLOBALS['wp_version'] !== $ver );
     235
     236    if ( $wp_gh_asset || $ver_not_wp ) {
     237        $src = sprintf(
     238            'https://cdn.jsdelivr.net/%s/%s@%s',
     239            $data->type,
     240            $data->name,
     241            $data->version . $data->file
     242        );
     243        add_integrity_to_asset( $type, $handle, $data->integrity );
     244    }
     245
     246    return $src;
     247}
     248
     249/**
     250 * Retrieves the value of a specific query argument from the given URL.
     251 *
     252 * @param string $url The URL containing the query parameters.
     253 * @param string $arg The name of the query argument to retrieve.
     254 * @return string|null The value of the specified query argument, or null if it is not found.
     255 */
     256function get_url_arg( string $url, string $arg ): ?string {
     257
     258    $query_string = parse_url( $url, PHP_URL_QUERY );
     259
     260    if ( empty( $query_string ) || ! is_string( $query_string ) ) {
     261        return null;
     262    }
     263
     264    parse_str( $query_string, $query_args );
     265
     266    return $query_args[ $arg ] ?? null;
     267}
     268
     269function gen_integrity( string $input ): string {
     270    $hash        = hash( 'sha384', $input, true );
     271    $hash_base64 = base64_encode( $hash ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
     272    return "sha384-$hash_base64";
     273}
     274
     275function path_from_url( string $url ): ?string {
     276    $parsed_url = wp_parse_url( $url );
    110277    $file       = rtrim( ABSPATH, '/' ) . $parsed_url['path'];
    111278    $file_alt   = rtrim( dirname( ABSPATH ), '/' ) . $parsed_url['path'];
    112279
    113280    if ( is_file( $file ) ) {
    114         $data = get_jsdeliver_hash_api_data( $file );
    115     };
    116     if ( is_file( $file_alt ) ) {
    117         $data = get_jsdeliver_hash_api_data( $file_alt );
    118     };
    119 
    120     if ( isset( $data['type'] ) && 'gh' === $data['type'] ) {
    121         $src = "https://cdn.jsdelivr.net/{$data['type']}/{$data['name']}@{$data['version']}{$data['file']}";
    122     }
    123 
    124     return $src;
    125 }
    126 
    127 function get_jsdeliver_hash_api_data( $file_path ) {
    128 
    129     $transient_name = "jsdelivr_this_hashapi_wp{$GLOBALS['wp_version']}_$file_path";
    130     $result         = get_transient( $transient_name );
    131 
    132     if ( false === $result ) {
    133 
    134         // Local file, no need for wp_remote_get
    135         // phpcs:disable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    136         $result       = array();
    137         $file_content = file_get_contents( $file_path );
    138         // phpcs:enable WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
    139 
    140         if ( $file_content ) {
    141             $sha256 = hash( 'sha256', $file_content );
    142             $data   = wp_safe_remote_get( "https://data.jsdelivr.com/v1/lookup/hash/$sha256", array() );
    143 
    144             if ( ! is_wp_error( $data ) ) {
    145                 $result = (array) json_decode( wp_remote_retrieve_body( $data ), true );
    146             }
    147         }
    148 
    149         set_transient( $transient_name, $result, wp_rand( DAY_IN_SECONDS, DAY_IN_SECONDS * 2 ) );
    150     }
    151 
    152     return $result;
    153 }
    154 
    155 function contains( $haystack, $needle ) {
    156     return strpos( $haystack, $needle ) !== false;
    157 }
    158 
    159 function starts_with( $haystack, $needle ) {
    160     return $haystack[0] === $needle[0] ? strncmp( $haystack, $needle, strlen( $needle ) ) === 0 : false;
    161 }
    162 
    163 function get_plugin_version( $plugin_file ) {
    164     $plugin_data = get_plugin_data( WP_PLUGIN_DIR . "/$plugin_file", false, false );
     281        return $file;
     282    } elseif ( is_file( $file_alt ) ) {
     283        return $file_alt;
     284    }
     285
     286    return null;
     287}
     288
     289function get_plugin_version( string $plugin_file ): string {
     290    $plugin_data = get_file_data( WP_PLUGIN_DIR . "/$plugin_file", array( 'Version' => 'Version' ), 'plugin' );
    165291    return $plugin_data['Version'];
    166292}
    167 
    168 /**
    169  * Parses the plugin contents to retrieve plugin's metadata.
    170  *
    171  * The metadata of the plugin's data searches for the following in the plugin's
    172  * header. All plugin data must be on its own line. For plugin description, it
    173  * must not have any newlines or only parts of the description will be displayed
    174  * and the same goes for the plugin data. The below is formatted for printing.
    175  *
    176  *     /*
    177  *     Plugin Name: Name of Plugin
    178  *     Plugin URI: Link to plugin information
    179  *     Description: Plugin Description
    180  *     Author: Plugin author's name
    181  *     Author URI: Link to the author's web site
    182  *     Version: Must be set in the plugin for WordPress 2.3+
    183  *     Text Domain: Optional. Unique identifier, should be same as the one used in
    184  *          load_plugin_textdomain()
    185  *     Domain Path: Optional. Only useful if the translations are located in a
    186  *          folder above the plugin's base path. For example, if .mo files are
    187  *          located in the locale folder then Domain Path will be "/locale/" and
    188  *          must have the first slash. Defaults to the base folder the plugin is
    189  *          located in.
    190  *     Network: Optional. Specify "Network: true" to require that a plugin is activated
    191  *          across all sites in an installation. This will prevent a plugin from being
    192  *          activated on a single site when Multisite is enabled.
    193  *      * / # Remove the space to close comment
    194  *
    195  * Some users have issues with opening large files and manipulating the contents
    196  * for want is usually the first 1kiB or 2kiB. This function stops pulling in
    197  * the plugin contents when it has all of the required plugin data.
    198  *
    199  * The first 8kiB of the file will be pulled in and if the plugin data is not
    200  * within that first 8kiB, then the plugin author should correct their plugin
    201  * and move the plugin data headers to the top.
    202  *
    203  * The plugin file is assumed to have permissions to allow for scripts to read
    204  * the file. This is not checked however and the file is only opened for
    205  * reading.
    206  *
    207  * @since 1.5.0
    208  *
    209  * @param string $plugin_file Path to the main plugin file.
    210  * @param bool   $markup      Optional. If the returned data should have HTML markup applied.
    211  *                            Default true.
    212  * @param bool   $translate   Optional. If the returned data should be translated. Default true.
    213  * @return array {
    214  *     Plugin data. Values will be empty if not supplied by the plugin.
    215  *
    216  *     @type string $Name        Name of the plugin. Should be unique.
    217  *     @type string $Title       Title of the plugin and link to the plugin's site (if set).
    218  *     @type string $Description Plugin description.
    219  *     @type string $Author      Author's name.
    220  *     @type string $AuthorURI   Author's website address (if set).
    221  *     @type string $Version     Plugin version.
    222  *     @type string $TextDomain  Plugin textdomain.
    223  *     @type string $DomainPath  Plugins relative directory path to .mo files.
    224  *     @type bool   $Network     Whether the plugin can only be activated network-wide.
    225  * }
    226  */
    227 function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
    228 
    229     $default_headers = array(
    230         'Name'        => 'Plugin Name',
    231         'PluginURI'   => 'Plugin URI',
    232         'Version'     => 'Version',
    233         'Description' => 'Description',
    234         'Author'      => 'Author',
    235         'AuthorURI'   => 'Author URI',
    236         'TextDomain'  => 'Text Domain',
    237         'DomainPath'  => 'Domain Path',
    238         'Network'     => 'Network',
    239         // Site Wide Only is deprecated in favor of Network.
    240         '_sitewide'   => 'Site Wide Only',
    241     );
    242 
    243     $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );
    244 
    245     // Site Wide Only is the old header for Network
    246     if ( ! $plugin_data['Network'] && $plugin_data['_sitewide'] ) {
    247         // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped
    248         // phpcs:disable WordPress.WP.I18n.MissingArgDomain
    249         /* translators: 1: Site Wide Only: true, 2: Network: true */
    250         _deprecated_argument( __FUNCTION__, '3.0.0', sprintf( __( 'The %1$s plugin header is deprecated. Use %2$s instead.' ), '<code>Site Wide Only: true</code>', '<code>Network: true</code>' ) );
    251         // phpcs:enable WordPress.Security.EscapeOutput.OutputNotEscaped
    252         // phpcs:enable WordPress.WP.I18n.MissingArgDomain
    253         $plugin_data['Network'] = $plugin_data['_sitewide'];
    254     }
    255     // phpcs:disable WordPress.PHP.StrictComparisons.LooseComparison
    256     $plugin_data['Network'] = ( 'true' == strtolower( $plugin_data['Network'] ) );
    257     // phpcs:enable WordPress.PHP.StrictComparisons.LooseComparison
    258     unset( $plugin_data['_sitewide'] );
    259 
    260     // If no text domain is defined fall back to the plugin slug.
    261     if ( ! $plugin_data['TextDomain'] ) {
    262         $plugin_slug = dirname( plugin_basename( $plugin_file ) );
    263         if ( '.' !== $plugin_slug && false === strpos( $plugin_slug, '/' ) ) {
    264             $plugin_data['TextDomain'] = $plugin_slug;
    265         }
    266     }
    267 
    268     if ( $markup || $translate ) {
    269         $plugin_data = _get_plugin_data_markup_translate( $plugin_file, $plugin_data, $markup, $translate );
    270��    } else {
    271         $plugin_data['Title']      = $plugin_data['Name'];
    272         $plugin_data['AuthorName'] = $plugin_data['Author'];
    273     }
    274 
    275     return $plugin_data;
    276 }
  • nextgenthemes-jsdelivr-this/trunk/readme.txt

    r2155264 r3086793  
    11=== NGT jsDelivr CDN ===
    22Contributors: nico23
    3 Tags: CDN, JS, JavaScript, jQuery, Performance, minimalistic, minimal
     3Tags: CDN, JS, JavaScript, j
    44Donate link: https://nextgenthemes.com/donate
    5 Requires at least: 4.3.1
    6 Requires PHP: 5.6
    7 Tested up to: 5.2.2
    8 Stable tag: 1.0.0
     5Requires at least:
     6Requires PHP:
     7Tested up to:
     8Stable tag: 1..0
    99License: GPL 3.0
    1010License URI: http://www.gnu.org/licenses/gpl-3.0.html
     
    1414== Changelog ==
    1515
     16
     17
     18
    1619= 2019-09-07 1.0.0 =
    17 * Run only once per page-load, better function names and some useful comments.
     20* Run only once per page-load.
     21* Better function names and some useful comments.
     22* Send this plugins url as user-agent to jsDelivr knows how its used. (They asked for this). This also means more privacy as the `wp_remote_get` referrer sends your site URL (I really do not like that)
    1823
    1924= 2019-08-31 0.9.4 =
Note: See TracChangeset for help on using the changeset viewer.