File: //home/mirz4654/www/wp-content/plugins/wp-rocket/inc/classes/optimization/JS/class-combine.php
<?php
namespace WP_Rocket\Optimization\JS;
use WP_Rocket\Admin\Options_Data as Options;
use WP_Rocket\Optimization\Assets_Local_Cache;
use WP_Rocket\Logger\Logger;
use MatthiasMullie\Minify;
/**
* Combines JS files
*
* @since 3.1
* @author Remy Perona
*/
class Combine extends Abstract_JS_Optimization {
/**
* Minifier instance
*
* @since 3.1
* @author Remy Perona
*
* @var Minify\JS
*/
private $minifier;
/**
* Assets local cache instance
*
* @since 3.1
* @author Remy Perona
*
* @var Assets_Local_Cache
*/
private $local_cache;
/**
* JQuery URL
*
* @since 3.1
* @author Remy Perona
*
* @var bool|string
*/
private $jquery_url;
/**
* Scripts to combine
*
* @since 3.1
* @author Remy Perona
*
* @var array
*/
private $scripts = [];
/**
* Inline scripts excluded from combined and moved after the combined file
*
* @since 3.1.4
* @author Remy Perona
*
* @var array
*/
private $move_after = [];
/**
* Constructor
*
* @since 3.1
* @author Remy Perona
*
* @param Options $options Plugin options instance.
* @param Minify\JS $minifier Minifier instance.
* @param Assets_Local_Cache $local_cache Assets local cache instance.
*/
public function __construct( Options $options, Minify\JS $minifier, Assets_Local_Cache $local_cache ) {
parent::__construct( $options );
$this->minifier = $minifier;
$this->local_cache = $local_cache;
$this->jquery_url = $this->get_jquery_url();
}
/**
* Minifies and combines JavaScripts into one
*
* @since 3.1
* @author Remy Perona
*
* @param string $html HTML content.
* @return string
*/
public function optimize( $html ) {
Logger::info( 'JS COMBINE PROCESS STARTED.', [ 'js combine process' ] );
$html_nocomments = $this->hide_comments( $html );
$scripts = $this->find( '<script.*<\/script>', $html_nocomments );
if ( ! $scripts ) {
Logger::debug( 'No `<script>` tags found.', [ 'js combine process' ] );
return $html;
}
Logger::debug( 'Found ' . count( $scripts ) . ' `<script>` tag(s).', [
'js combine process',
'tags' => $scripts,
] );
$combine_scripts = $this->parse( $scripts );
if ( empty( $combine_scripts ) ) {
Logger::debug( 'No `<script>` tags to optimize.', [ 'js combine process' ] );
return $html;
}
Logger::debug( count( $combine_scripts ) . ' `<script>` tag(s) remaining.', [
'js combine process',
'tags' => $combine_scripts,
] );
$content = $this->get_content();
if ( empty( $content ) ) {
Logger::debug( 'No JS content.', [ 'js combine process' ] );
return $html;
}
$minify_url = $this->combine( $content );
if ( ! $minify_url ) {
Logger::error( 'JS combine process failed.', [ 'js combine process' ] );
return $html;
}
$move_after = '';
if ( ! empty( $this->move_after ) ) {
foreach ( $this->move_after as $script ) {
$move_after .= $script;
$html = str_replace( $script, '', $html );
}
}
$html = str_replace( '</body>', '<script src="' . esc_url( $minify_url ) . '" data-minify="1"></script>' . $move_after . '</body>', $html );
foreach ( $combine_scripts as $script ) {
$html = str_replace( $script[0], '', $html );
}
Logger::info( 'Combined JS file successfully added.', [
'js combine process',
'url' => $minify_url,
] );
return $html;
}
/**
* Parses found nodes to keep only the ones to combine
*
* @since 3.1
* @author Remy Perona
*
* @param Array $scripts scripts corresponding to JS file or content.
* @return array
*/
protected function parse( $scripts ) {
$scripts = array_map( function( $script ) {
preg_match( '/<script\s+([^>]+[\s\'"])?src\s*=\s*[\'"]\s*?([^\'"]+\.js(?:\?[^\'"]*)?)\s*?[\'"]([^>]+)?\/?>/Umsi', $script[0], $matches );
if ( isset( $matches[2] ) ) {
if ( $this->is_external_file( $matches[2] ) ) {
foreach ( $this->get_excluded_external_file_path() as $excluded_file ) {
if ( false !== strpos( $matches[2], $excluded_file ) ) {
Logger::debug( 'Script is external.', [
'js combine process',
'tag' => $matches[0],
] );
return;
}
}
$this->scripts[] = [
'type' => 'url',
'content' => $matches[2],
];
return $script;
}
if ( $this->is_minify_excluded_file( $matches ) ) {
Logger::debug( 'Script is excluded.', [
'js combine process',
'tag' => $matches[0],
] );
return;
}
if ( $this->jquery_url && false !== strpos( $matches[2], $this->jquery_url ) ) {
Logger::debug( 'Script is jQuery.', [
'js combine process',
'tag' => $matches[0],
] );
return;
}
$file_path = $this->get_file_path( $matches[2] );
if ( ! $file_path ) {
return;
}
$this->scripts[] = [
'type' => 'file',
'content' => $file_path,
];
} else {
preg_match( '/<script\b(?<attrs>[^>]*)>(?:\/\*\s*<!\[CDATA\[\s*\*\/)?\s*(?<content>[\s\S]*?)\s*(?:\/\*\s*\]\]>\s*\*\/)?<\/script>/msi', $script[0], $matches_inline );
if ( preg_last_error() == PREG_BACKTRACK_LIMIT_ERROR ) {
Logger::debug( 'PCRE regex execution Catastrophic Backtracking', [
'inline JS backtracking error',
'content' => $matches_inline['content'],
] );
return;
}
if ( strpos( $matches_inline['attrs'], 'type' ) !== false && ! preg_match( '/type\s*=\s*["\']?(?:text|application)\/(?:(?:x\-)?javascript|ecmascript)["\']?/i', $matches_inline['attrs'] ) ) {
Logger::debug( 'Inline script is not JS.', [
'js combine process',
'attributes' => $matches_inline['attrs'],
] );
return;
}
if ( false !== strpos( $matches_inline['attrs'], 'src=' ) ) {
Logger::debug( 'Inline script has a `src` attribute.', [
'js combine process',
'attributes' => $matches_inline['attrs'],
] );
return;
}
if ( in_array( $matches_inline['content'], $this->get_localized_scripts(), true ) ) {
Logger::debug(
'Inline script is a localize script',
[
'js combine process',
'excluded_content' => $matches_inline['content'],
]
);
return;
}
foreach ( $this->get_excluded_inline_content() as $excluded_content ) {
if ( false !== strpos( $matches_inline['content'], $excluded_content ) ) {
Logger::debug( 'Inline script has excluded content.', [
'js combine process',
'excluded_content' => $excluded_content,
] );
return;
}
}
foreach ( $this->get_move_after_inline_scripts() as $move_after_script ) {
if ( false !== strpos( $matches_inline['content'], $move_after_script ) ) {
$this->move_after[] = $script[0];
return;
}
}
$this->scripts[] = [
'type' => 'inline',
'content' => $matches_inline['content'],
];
}
return $script;
}, $scripts );
return array_filter( $scripts );
}
/**
* Gets content for each script either from inline or from src
*
* @since 3.1
* @author Remy Perona
*
* @return string
*/
protected function get_content() {
$content = '';
foreach ( $this->scripts as $script ) {
if ( 'file' === $script['type'] ) {
$file_content = $this->get_file_content( $script['content'] );
$content .= $file_content;
$this->add_to_minify( $file_content );
} elseif ( 'url' === $script['type'] ) {
$file_content = $this->local_cache->get_content( rocket_add_url_protocol( $script['content'] ) );
$content .= $file_content;
$this->add_to_minify( $file_content );
} elseif ( 'inline' === $script['type'] ) {
$inline_js = rtrim( $script['content'], ";\n\t\r" ) . ';';
$content .= $inline_js;
$this->add_to_minify( $inline_js );
}
}
return $content;
}
/**
* Creates the minify URL if the minification is successful
*
* @since 2.11
* @author Remy Perona
*
* @param string $content Content to minify & combine.
* @return string|bool The minify URL if successful, false otherwise
*/
protected function combine( $content ) {
if ( empty( $content ) ) {
return false;
}
$filename = md5( $content . $this->minify_key ) . '.js';
$minified_file = $this->minify_base_path . $filename;
if ( ! rocket_direct_filesystem()->is_readable( $minified_file ) ) {
$minified_content = $this->minify();
if ( ! $minified_content ) {
return false;
}
$minify_filepath = $this->write_file( $minified_content, $minified_file );
if ( ! $minify_filepath ) {
return false;
}
}
return $this->get_minify_url( $filename );
}
/**
* Minifies the content
*
* @since 2.11
* @author Remy Perona
*
* @return string|bool Minified content, false if empty
*/
protected function minify() {
$minified_content = $this->minifier->minify();
if ( empty( $minified_content ) ) {
return false;
}
return $minified_content;
}
/**
* Adds content to the minifier
*
* @since 3.1
* @author Remy Perona
*
* @param string $content Content to minify/combine.
* @return void
*/
protected function add_to_minify( $content ) {
$this->minifier->add( $content );
}
/**
* Patterns in content excluded from being combined
*
* @since 3.1
* @author Remy Perona
*
* @return array
*/
protected function get_excluded_inline_content() {
$defaults = [
'document.write',
'google_ad',
'edToolbar',
'gtag',
'_gaq.push',
'_gaLt',
'GoogleAnalyticsObject',
'syntaxhighlighter',
'adsbygoogle',
'ci_cap_',
'_stq',
'nonce',
'post_id',
'LogHuman',
'idcomments_acct',
'ch_client',
'sc_online_t',
'_stq',
'bannersnack_embed',
'vtn_player_type',
'ven_video_key',
'ANS_customer_id',
'tdBlock',
'tdLocalCache',
'wpRestNonce',
'"url":',
'lazyLoadOptions',
'adthrive',
'loadCSS',
'google_tag_params',
'clicky_custom',
'clicky_site_ids',
'NSLPopupCenter',
'_paq',
'gtm',
'dataLayer',
'RecaptchaLoad',
'WPCOM_sharing_counts',
'jetpack_remote_comment',
'subscribe-field',
'contextly',
'_mmunch',
'gt_request_uri',
'doGTranslate',
'docTitle',
'bs_ajax_paginate_',
'bs_deferred_loading_',
'theChampRedirectionUrl',
'theChampFBCommentUrl',
'theChampTwitterRedirect',
'theChampRegRedirectionUrl',
'ESSB_CACHE_URL',
'oneall_social_login_providers_',
'betterads_screen_width',
'woocommerce_wishlist_add_to_wishlist_url',
'arf_conditional_logic',
'heateorSsHorSharingShortUrl',
'TL_Const',
'bimber_front_microshare',
'setAttribute("id"',
'setAttribute( "id"',
'TribeEventsPro',
'peepsotimedata',
'wphc_data',
'hc_rand_id',
'RBL_ADD',
'AfsAnalyticsObject',
'_thriveCurrentPost',
'esc_login_url',
'fwduvpMainPlaylist',
'Bibblio.initRelatedContent',
'showUFC()',
'#iphorm-',
'#fancy-',
'ult-carousel-',
'theChampLJAuthUrl',
'f._fbq',
'Insticator',
'w2dc_js_objects',
'cherry_ajax',
'ad_block_',
'elementorFrontendConfig',
'omapi_localized',
'zeen_',
'disqusIdentifier',
'currentAjaxUrl',
'geodir_event_call_calendar_',
'atatags-',
'hbspt.forms.create',
'function(c,h,i,m,p)',
'dataTable({',
'rankMath = {',
'_atrk_opts',
'quicklinkOptions',
'ct_checkjs_',
'WP_Statistics_http',
'penci_block_',
'omapi_data',
'tminusnow',
'nfForms',
'galleries.gallery_',
'wcj_evt.prodID',
'advads_tracking_ads',
'advadsGATracking.postContext',
'woopack_config',
'ulp_content_id',
'wp-cumulus/tagcloud.swf?r=',
'ctSetCookie(\'ct_checkjs\'',
'woof_really_curr_tax',
'uLogin.customInit',
'i18n_no_matching_variations_text',
'alsp_map_markers_attrs',
'var inc_opt =',
'iworks_upprev',
'yith_wcevti_tickets',
'window.metrilo.ensure_cbuid',
'metrilo.event',
'wordpress_page_root',
'wcct_info',
'Springbot.product_id',
'pysWooProductData',
'dfd-heading',
'owl=$("#',
'penci_megamenu',
'fts_security',
'algoliaAutocomplete',
'avia_framework_globals',
'tabs.easyResponsiveTabs',
'searchlocationHeader',
'yithautocomplete',
'data-parallax-speed',
'currency_data=',
'cedexisData',
'function reenableButton',
'#wpnbio-show',
'e.Newsletter2GoTrackingObject',
'var categories_',
'"+nRemaining+"',
'cartsguru_cart_token',
'after_share_easyoptin',
'location_data.push',
'thirstyFunctions.isThirstyLink',
'styles: \' #custom-menu-',
'function svc_center_',
'#svc_carousel2_container_',
'advads.move',
'elementid',
'advads_has_ads',
'wpseo_map_init',
'mdf_current_page_url',
'tptn_tracker',
'dpsp_pin_button_data',
'searchwp_live_search_params',
'wpp_params',
'top.location,thispage',
'selection+pagelink',
'ic_window_resolution',
'PHP.wp_p_id',
'ShopifyBuy.UI.onReady(client)',
'orig_request_uri',
'gie.widgets.load',
'Adman.Flash',
'PHP.wp_p_id',
'window.broadstreetKeywords',
'var productId =',
'var flatsomeVars',
'wc_product_block_data',
'static.mailerlite.com',
];
$excluded_inline = array_merge( $defaults, $this->options->get( 'exclude_inline_js', [] ) );
/**
* Filters inline JS excluded from being combined
*
* @since 3.1
*
* @param array $pattern Patterns to match.
*/
return apply_filters( 'rocket_excluded_inline_js_content', $excluded_inline );
}
/**
* Patterns in URL excluded from being combined
*
* @since 3.1
* @author Remy Perona
*
* @return array
*/
protected function get_excluded_external_file_path() {
$defaults = [
'html5.js',
'show_ads.js',
'histats.com/js',
'ws.amazon.com/widgets',
'/ads/',
'intensedebate.com',
'scripts.chitika.net/',
'jotform.com/',
'gist.github.com',
'forms.aweber.com',
'video.unrulymedia.com',
'stats.wp.com',
'stats.wordpress.com',
'widget.rafflecopter.com',
'widget-prime.rafflecopter.com',
'releases.flowplayer.org',
'c.ad6media.fr',
'cdn.stickyadstv.com',
'www.smava.de',
'contextual.media.net',
'app.getresponse.com',
'adserver.reklamstore.com',
's0.wp.com',
'wprp.zemanta.com',
'files.bannersnack.com',
'smarticon.geotrust.com',
'js.gleam.io',
'ir-na.amazon-adsystem.com',
'web.ventunotech.com',
'verify.authorize.net',
'ads.themoneytizer.com',
'embed.finanzcheck.de',
'imagesrv.adition.com',
'js.juicyads.com',
'form.jotformeu.com',
'speakerdeck.com',
'content.jwplatform.com',
'ads.investingchannel.com',
'app.ecwid.com',
'www.industriejobs.de',
's.gravatar.com',
'googlesyndication.com',
'a.optmstr.com',
'a.optmnstr.com',
'a.opmnstr.com',
'adthrive.com',
'mediavine.com',
'js.hsforms.net',
'googleadservices.com',
'f.convertkit.com',
'recaptcha/api.js',
'mailmunch.co',
'apps.shareaholic.com',
'dsms0mj1bbhn4.cloudfront.net',
'nutrifox.com',
'code.tidio.co',
'www.uplaunch.com',
'widget.reviewability.com',
'embed-cdn.gettyimages.com/widgets.js',
'app.mailerlite.com',
'ck.page',
];
$excluded_external = array_merge( $defaults, $this->options->get( 'exclude_js', [] ) );
/**
* Filters JS externals files to exclude from the combine process
*
* @since 2.2
*
* @param array $pattern Patterns to match.
*/
return apply_filters( 'rocket_minify_excluded_external_js', $excluded_external );
}
/**
* Patterns of inline JS to move after the combined JS file
*
* @since 3.1.4
* @author Remy Perona
*
* @return array
*/
protected function get_move_after_inline_scripts() {
$move_after_scripts = [
'map_fusion_map_',
'ec:addProduct',
'ec:addImpression',
'clear_better_facebook_comments',
'vc-row-destroy-equal-heights-',
'dfd-icon-list-',
'SFM_template',
'WLTChangeState',
'wlt_star_',
'wlt_pop_distance_',
'smart_list_tip',
'gd-wgt-pagi-',
'data-rf-id=',
'tvc_po=',
'scrapeazon',
'startclock',
'it_logo_field_owl-box_',
'td_live_css_uid',
'wpvl_paramReplace',
'tdAjaxCount',
'mec_skin_',
'_wca',
'_taboola',
'fbq(\'trackCustom\'',
'fbq(\'track\'',
'data.token',
'sharrre',
'dfads_ajax_load_ads',
'tie_postviews',
'wmp_update',
'h5ab-print-article',
'gform_ajax_frame_',
'gform_post_render',
'mts_view_count',
'act_css_tooltip',
'window.SLB',
'wpt_view_count',
'var dateNow',
'gallery_product_',
'.flo-block-slideshow-',
'data=\'api-key=ct-',
'ip_common_function()',
'("style#gsf-custom-css").append',
'a3revWCDynamicGallery_',
'#owl-carousel-instagram-',
'window.FlowFlowOpts',
'jQuery(\'.td_uid_',
'jQuery(".slider-',
'#dfd-vcard-widget-',
'#sf-instagram-widget-',
'.woocommerce-tabs-',
'penci_megamenu__',
'vc_prepareHoverBox',
'wp-temp-form-div',
'_wswebinarsystem_already_',
'#views-extra-css").text',
'fusetag.setTargeting',
'hit.uptrendsdata.com',
'callback:window.renderBadge',
'test_run_nf_conditional_logic',
'cb_nombre',
'$(\'.fl-node-',
'function($){google_maps_',
'$("#myCarousel',
'et_animation_data=',
'current_url="',
'CustomEvent.prototype=window.Event.prototype',
'electro-wc-product-gallery',
'woof_is_mobile',
'jQuery(\'.videonextup',
'wpp_params',
'us.templateDirectoryUri=',
'.fat-gallery-item',
'.ratingbox',
'user_rating.prototype.eraseCookie',
'test_run_nf_conditional',
'dpsp-networks-btns-wrapper',
'pa_woo_product_info',
'sharing_enabled_on_post_via_metabox',
'#product-search-field-',
'GOTMLS_login_offset',
'berocket_aapf_time_to_fix_products_style',
'window.vc_googleMapsPointer',
'sinceID_',
'#ut-background-video-ut-section',
'+window.comment_tab_width+',
'dfd-button-hover-in',
'wpseo-address-wrapper',
'platform.stumbleupon.com',
];
/**
* Filters inline JS to move after the combined JS file
*
* @since 3.1.4
* @author Remy Perona
*
* @param array $move_after_scripts Patterns to match.
*/
return apply_filters( 'rocket_move_after_combine_js', $move_after_scripts );
}
/**
* Gets all localized scripts data to exclude them from combine.
*
* @since 3.1.3
* @author Remy Perona
*
* @return array
*/
protected function get_localized_scripts() {
static $localized_scripts;
if ( isset( $localized_scripts ) ) {
return $localized_scripts;
}
$localized_scripts = [];
foreach ( array_unique( wp_scripts()->queue ) as $item ) {
$data = wp_scripts()->print_extra_script( $item, false );
if ( empty( $data ) ) {
continue;
}
$localized_scripts[] = $data;
}
return $localized_scripts;
}
}