wp_back/wp-content/plugins/wp-asset-clean-up/classes/OptimiseAssets/CombineJs.php
2024-05-20 15:37:46 +03:00

921 lines
38 KiB
PHP

<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Menu;
use WpAssetCleanUp\FileSystem;
use WpAssetCleanUp\Misc;
use WpAssetCleanUp\ObjectCache;
/**
* Class CombineJs
* @package WpAssetCleanUp\OptimiseAssets
*/
class CombineJs
{
/**
* @var string
*/
public static $jsonStorageFile = 'js-combined{maybe-extra-info}.json';
/**
* @param $htmlSource
*
* @return mixed
*/
public static function doCombine($htmlSource)
{
if ( ! Misc::isDOMDocumentOn() ) {
return $htmlSource;
}
if ( ! self::proceedWithJsCombine() ) {
return $htmlSource;
}
global $wp_scripts;
$wpacuRegisteredScripts = $wp_scripts->registered;
$combineLevel = 2;
$isDeferAppliedOnBodyCombineGroupNo = false;
// $uriToFinalJsFile will always be relative ONLY within WP_CONTENT_DIR . self::getRelPathJsCacheDir()
// which is usually "wp-content/cache/asset-cleanup/js/"
// "true" would make it avoid checking the cache and always use the DOM Parser / RegExp
// for DEV purposes ONLY as it uses more resources
$finalCacheList = array();
$skipCache = false;
if (isset($_GET['wpacu_no_cache']) || (defined('WPACU_NO_CACHE') && WPACU_NO_CACHE === true)) {
$skipCache = true;
}
if (! $skipCache) {
// Speed up processing by getting the already existing final CSS file URI
// This will avoid parsing the HTML DOM and determine the combined URI paths for all the CSS files
$finalCacheList = OptimizeCommon::getAssetCachedData( self::$jsonStorageFile, OptimizeJs::getRelPathJsCacheDir(), 'js' );
}
if ( $skipCache || empty($finalCacheList) ) {
/*
* NO CACHING TRANSIENT; Parse the DOM
*/
// Nothing in the database records or the retrieved cached file does not exist?
OptimizeCommon::clearAssetCachedData(self::$jsonStorageFile);
$combinableList = array();
$jQueryMigrateInBody = false;
$jQueryLibInBodyCount = 0;
$minifyJsInlineTagsIsNotEnabled = ! (MinifyJs::isMinifyJsEnabled() && in_array(Main::instance()->settings['minify_loaded_js_for'], array('inline', 'all')));
if ($minifyJsInlineTagsIsNotEnabled) {
$domTag = Misc::initDOMDocument();
// Strip irrelevant tags to boost the speed of the parser (e.g. NOSCRIPT / SCRIPT(inline) / STYLE)
// Sometimes, inline CODE can be too large, and it takes extra time for loadHTML() to parse
$htmlSourceAlt = preg_replace( '@<script(| (type=(\'|"|)text/(javascript|template|html)(\'|"|)))>.*?</script>@si', '', $htmlSource );
$htmlSourceAlt = preg_replace( '@<(style|noscript)[^>]*?>.*?</\\1>@si', '', $htmlSourceAlt );
$htmlSourceAlt = preg_replace( '#<link([^<>]+)/?>#iU', '', $htmlSourceAlt );
if (Main::instance()->isFrontendEditView) {
$htmlSourceAlt = preg_replace( '@<form action="#wpacu_wrap_assets" method="post">.*?</form>@si', '', $htmlSourceAlt );
}
if ($htmlSourceAlt === '') {
$htmlSourceAlt = $htmlSource;
}
$domTag->loadHTML( $htmlSourceAlt );
} else {
$domTag = OptimizeCommon::getDomLoadedTag($htmlSource, 'combineJs');
}
// Only keep combinable JS files
foreach ( array( 'head', 'body' ) as $docLocationScript ) {
$groupIndex = 1;
$docLocationElements = $domTag->getElementsByTagName($docLocationScript)->item(0);
if ($docLocationElements === null) { continue; }
// High accuracy (e.g. it ignores tags inside HTML comments, conditional or not)
$scriptTags = $docLocationElements->getElementsByTagName('script');
if ($scriptTags === null) { continue; }
if ($docLocationScript && Main::instance()->settings['combine_loaded_js_defer_body']) {
ObjectCache::wpacu_cache_set('wpacu_html_dom_body_tag_for_js', $docLocationElements);
}
foreach ($scriptTags as $tagObject) {
$scriptAttributes = array();
if ( isset($tagObject->attributes) && ! empty($tagObject->attributes) ) {
foreach ( $tagObject->attributes as $attrObj ) {
$scriptAttributes[ $attrObj->nodeName ] = trim( $attrObj->nodeValue );
}
}
$scriptNotCombinable = false; // default (usually, most of the SCRIPT tags can be optimized)
// Check if the CSS file has any 'data-wpacu-skip' attribute; if it does, do not alter it
if (isset($scriptAttributes['data-wpacu-skip'])) {
$scriptNotCombinable = true;
}
$handleToCheck = isset($scriptAttributes['data-wpacu-script-handle']) ? $scriptAttributes['data-wpacu-script-handle'] : ''; // Maybe: JS Inline (Before, After)
$hasSrc = isset($scriptAttributes['src']) && trim($scriptAttributes['src']); // No valid SRC attribute? It's not combinable (e.g. an inline tag)
$isPluginScript = isset($scriptAttributes['data-wpacu-plugin-script']); // Only of the user is logged-in (skip it as it belongs to the Asset CleanUp (Pro) plugin)
if (! $scriptNotCombinable && (! $hasSrc || $isPluginScript)) {
// Inline tag? Skip it in the BODY
if ($docLocationScript === 'body') {
continue;
}
// Because of jQuery, we will not have the list of all inline scripts and then the combined files as it is in BODY
if ($docLocationScript === 'head') {
if ($handleToCheck === '' && isset($scriptAttributes['id'])) {
$replaceToGetHandle = '';
if (strpos($scriptAttributes['id'], '-js-extra') !== false) { $replaceToGetHandle = '-js-extra'; }
if (strpos($scriptAttributes['id'], '-js-before') !== false) { $replaceToGetHandle = '-js-before'; }
if (strpos($scriptAttributes['id'], '-js-after') !== false) { $replaceToGetHandle = '-js-after'; }
if (strpos($scriptAttributes['id'], '-js-translations') !== false) { $replaceToGetHandle = '-js-translations'; }
if ($replaceToGetHandle) {
$handleToCheck = str_replace( $replaceToGetHandle, '', $scriptAttributes['id'] ); // Maybe: JS Inline (Data)
}
}
// Once an inline SCRIPT (with few exceptions below), except the ones associated with an enqueued script tag (with "src") is stumbled upon, a new combined group in the HEAD tag will be formed
if ($handleToCheck && OptimizeCommon::appendInlineCodeToCombineAssetType('js')) {
$getInlineAssociatedWithHandle = OptimizeJs::getInlineAssociatedWithScriptHandle($handleToCheck, $wpacuRegisteredScripts, 'handle');
if ( ($getInlineAssociatedWithHandle['data'] || $getInlineAssociatedWithHandle['before'] || $getInlineAssociatedWithHandle['after'])
|| in_array(trim($tagObject->nodeValue), array($getInlineAssociatedWithHandle['data'], $getInlineAssociatedWithHandle['before'], $getInlineAssociatedWithHandle['after']))
|| (strpos(trim($tagObject->nodeValue), '/* <![CDATA[ */') === 0 && Misc::endsWith(trim($tagObject->nodeValue), '/* ]]> */')) ) {
// It's associated with the enqueued scripts, or it's a (standalone) CDATA inline tag added via wp_localize_script()
// Skip it instead and if the CDATA is not standalone (e.g. not associated with any script tag), the loop will "stay" in the same combined group
continue;
}
}
$scriptNotCombinable = true;
}
}
$isInGroupType = 'standard';
$isJQueryLib = $isJQueryMigrate = false;
// Has SRC and $isPluginScript is set to false OR it does not have "data-wpacu-skip" attribute
if (! $scriptNotCombinable) {
$src = (string)$scriptAttributes['src'];
if (self::skipCombine($src, $handleToCheck)) {
$scriptNotCombinable = true;
}
// Avoid any errors when code like the following one is used:
// wp.i18n.setLocaleData( localeData, domain );
// Because the inline JS is not appended to the combined JS, /wp-includes/js/dist/i18n.(min).js has to be called earlier (outside the combined JS file)
if ( ! OptimizeCommon::appendInlineCodeToCombineAssetType('js') && (strpos($src, '/wp-includes/js/dist/i18n.') !== false) ) {
$scriptNotCombinable = true;
}
if (isset($scriptAttributes['data-wpacu-to-be-preloaded-basic']) && $scriptAttributes['data-wpacu-to-be-preloaded-basic']) {
$scriptNotCombinable = true;
}
// Was it optimized and has the URL updated? Check the Source URL
if (! $scriptNotCombinable && isset($scriptAttributes['data-wpacu-script-rel-src-before']) && $scriptAttributes['data-wpacu-script-rel-src-before'] && self::skipCombine($scriptAttributes['data-wpacu-script-rel-src-before'], $handleToCheck)) {
$scriptNotCombinable = true;
}
$isJQueryLib = isset($scriptAttributes['data-wpacu-jquery-core-handle']);
$isJQueryMigrate = isset($scriptAttributes['data-wpacu-jquery-migrate-handle']);
if (isset($scriptAttributes['async'], $scriptAttributes['defer'])) { // Has both "async" and "defer"
$isInGroupType = 'async_defer';
} elseif (isset($scriptAttributes['async'])) { // Has only "async"
$isInGroupType = 'async';
} elseif (isset($scriptAttributes['defer'])) { // Has only "defer"
// Does it have "defer" attribute, it's combinable (all checks were already done), loads in the BODY tag and "combine_loaded_js_defer_body" is ON? Keep it to the combination list
$isCombinableWithBodyDefer = (! $scriptNotCombinable && $docLocationScript === 'body' && Main::instance()->settings['combine_loaded_js_defer_body']);
if (! $isCombinableWithBodyDefer) {
$isInGroupType = 'defer'; // Otherwise, add it to the "defer" group type
}
}
}
if ( ! $scriptNotCombinable ) {
// It also checks the domain name to make sure no external scripts would be added to the list
if ( $localAssetPath = OptimizeCommon::getLocalAssetPath( $src, 'js' ) ) {
$scriptExtra = array();
if ( isset( $scriptAttributes['data-wpacu-script-handle'], $wpacuRegisteredScripts[ $scriptAttributes['data-wpacu-script-handle'] ]->extra ) && OptimizeCommon::appendInlineCodeToCombineAssetType('js') ) {
$scriptExtra = $wpacuRegisteredScripts[ $scriptAttributes['data-wpacu-script-handle'] ]->extra;
$anyScriptTranslations = method_exists('wp_scripts', 'print_translations')
? wp_scripts()->print_translations( $scriptAttributes['data-wpacu-script-handle'], false )
: false;
if ( $anyScriptTranslations ) {
$scriptExtra['translations'] = $anyScriptTranslations;
}
}
// Standard (could be multiple groups per $docLocationScript), Async & Defer, Async, Defer
$groupByType = ($isInGroupType === 'standard') ? $groupIndex : $isInGroupType;
if ($docLocationScript === 'body') {
if ($isJQueryLib || strpos($localAssetPath, '/wp-includes/js/jquery/jquery.js') !== false) {
$jQueryLibInBodyCount++;
}
if ($isJQueryMigrate || strpos($localAssetPath, '/wp-includes/js/jquery/jquery-migrate') !== false) {
$jQueryLibInBodyCount++;
$jQueryMigrateInBody = true;
}
}
$combinableList[$docLocationScript][$groupByType][] = array(
'src' => $src,
'local' => $localAssetPath,
'info' => array(
'is_jquery' => $isJQueryLib,
'is_jquery_migrate' => $isJQueryMigrate
),
'extra' => $scriptExtra
);
if ($docLocationScript === 'body' && $jQueryLibInBodyCount === 2) {
$jQueryLibInBodyCount = 0; // reset it
$groupIndex ++; // a new JS group will be created if jQuery & jQuery Migrate are combined in the BODY
continue;
}
}
} else {
$groupIndex ++; // a new JS group will be created (applies to "standard" ones only)
}
}
}
// Could be pages such as maintenance mode with no external JavaScript files
if (empty($combinableList)) {
return $htmlSource;
}
$finalCacheList = array();
foreach ($combinableList as $docLocationScript => $combinableListGroups) {
$groupNo = 1;
foreach ($combinableListGroups as $groupType => $groupFiles) {
// Any groups having one file? Then it's not really a group and the file should load on its own
// Could be one extra file besides the jQuery & jQuery Migrate group or the only JS file called within the HEAD
if (count($groupFiles) < 2) {
continue;
}
$localAssetsPaths = $groupScriptSrcs = array();
$localAssetsExtra = array();
$jQueryIsIncludedInGroup = false;
foreach ($groupFiles as $groupFileData) {
if ($groupFileData['info']['is_jquery'] || strpos($groupFileData['local'], '/wp-includes/js/jquery/jquery.js') !== false) {
$jQueryIsIncludedInGroup = true;
// Is jQuery in the BODY without jQuery Migrate loaded?
// Isolate it as it needs to be the first to load in case there are inline scripts calling it before the combined group(s)
if ($docLocationScript === 'body' && ! $jQueryMigrateInBody) {
continue;
}
}
$src = $groupFileData['src'];
$groupScriptSrcs[] = $src;
$localAssetsPaths[$src] = $groupFileData['local'];
$localAssetsExtra[$src] = $groupFileData['extra'];
}
$maybeDoJsCombine = self::maybeDoJsCombine(
$localAssetsPaths,
$localAssetsExtra,
$docLocationScript
);
// Local path to combined CSS file
$localFinalJsFile = $maybeDoJsCombine['local_final_js_file'];
// URI (e.g. /wp-content/cache/asset-cleanup/[file-name-here.js]) to the combined JS file
$uriToFinalJsFile = $maybeDoJsCombine['uri_final_js_file'];
if (! is_file($localFinalJsFile)) {
return $htmlSource; // something is not right as the file wasn't created, we will return the original HTML source
}
$groupScriptSrcsFilter = array_map(static function($src) {
$src = str_replace(site_url(), '', $src);
// Starts with // (protocol is missing) - the replacement above wasn't made
if (strpos($src, '//') === 0) {
$siteUrlNoProtocol = str_replace(array('http:', 'https:'), '', site_url());
return str_replace($siteUrlNoProtocol, '', $src);
}
return $src;
}, $groupScriptSrcs);
$finalCacheList[$docLocationScript][$groupNo] = array(
'uri_to_final_js_file' => $uriToFinalJsFile,
'script_srcs' => $groupScriptSrcsFilter
);
if (in_array($groupType, array('async_defer', 'async', 'defer'))) {
if ($groupType === 'async_defer') {
$finalCacheList[$docLocationScript][$groupNo]['extra_attributes'][] = 'async';
$finalCacheList[$docLocationScript][$groupNo]['extra_attributes'][] = 'defer';
} else {
$finalCacheList[$docLocationScript][$groupNo]['extra_attributes'][] = $groupType;
}
}
// Apply 'defer="defer"' to combined JS files from the BODY tag (if enabled), except the combined jQuery & jQuery Migrate Group
if ($docLocationScript === 'body' && ! $jQueryIsIncludedInGroup && Main::instance()->settings['combine_loaded_js_defer_body']) {
if ($isDeferAppliedOnBodyCombineGroupNo === false) {
// Only record the first one
$isDeferAppliedOnBodyCombineGroupNo = $groupNo;
}
$finalCacheList[$docLocationScript][$groupNo]['extra_attributes'][] = 'defer';
}
$groupNo ++;
}
}
OptimizeCommon::setAssetCachedData(self::$jsonStorageFile, OptimizeJs::getRelPathJsCacheDir(), wp_json_encode($finalCacheList));
}
if (! empty($finalCacheList)) {
$cdnUrls = OptimizeCommon::getAnyCdnUrls();
$cdnUrlForJs = isset($cdnUrls['js']) ? $cdnUrls['js'] : false;
foreach ( $finalCacheList as $docLocationScript => $cachedGroupsList ) {
foreach ($cachedGroupsList as $groupNo => $cachedValues) {
$htmlSourceBeforeGroupReplacement = $htmlSource;
$uriToFinalJsFile = $cachedValues['uri_to_final_js_file'];
$filesSources = $cachedValues['script_srcs'];
// Basic Combining (1) -> replace "first" tag with the final combination tag (there would be most likely multiple groups)
// Enhanced Combining (2) -> replace "last" tag with the final combination tag (most likely one group)
$indexReplacement = ($combineLevel === 2) ? (count($filesSources) - 1) : 0;
$finalTagUrl = OptimizeCommon::filterWpContentUrl($cdnUrlForJs) . OptimizeJs::getRelPathJsCacheDir() . $uriToFinalJsFile;
$finalJsTagAttrsOutput = '';
$extraAttrs = array();
if (isset($cachedValues['extra_attributes']) && ! empty($cachedValues['extra_attributes'])) {
$extraAttrs = $cachedValues['extra_attributes'];
foreach ($extraAttrs as $finalJsTagAttr) {
$finalJsTagAttrsOutput .= ' '.$finalJsTagAttr.'=\''.$finalJsTagAttr.'\' ';
}
$finalJsTagAttrsOutput = trim($finalJsTagAttrsOutput);
}
// No async or defer? Add the preloading for the combined JS from the BODY
if ( ! $finalJsTagAttrsOutput && $docLocationScript === 'body' ) {
$finalJsTagAttrsOutput = ' data-wpacu-to-be-preloaded-basic=\'1\' ';
if ( ! defined('WPACU_REAPPLY_PRELOADING_FOR_COMBINED_JS') ) { define('WPACU_REAPPLY_PRELOADING_FOR_COMBINED_JS', true); }
}
// e.g. For developers that might want to add custom attributes such as data-cfasync="false"
$finalJsTag = apply_filters(
'wpacu_combined_js_tag',
'<script '.$finalJsTagAttrsOutput.' '.Misc::getScriptTypeAttribute().' id=\'wpacu-combined-js-'.$docLocationScript.'-group-'.$groupNo.'\' src=\''.$finalTagUrl.'\'></script>',
array(
'attrs' => $extraAttrs,
'doc_location' => $docLocationScript,
'group_no' => $groupNo,
'src' => $finalTagUrl
)
);
// Reference: https://stackoverflow.com/questions/2368539/php-replacing-multiple-spaces-with-a-single-space
$finalJsTag = preg_replace('!\s+!', ' ', $finalJsTag);
$scriptTagsStrippedNo = 0;
$scriptTags = OptimizeJs::getScriptTagsFromSrcs($filesSources, $htmlSource);
foreach ($scriptTags as $groupScriptTagIndex => $scriptTag) {
$replaceWith = ($groupScriptTagIndex === $indexReplacement) ? $finalJsTag : '';
$htmlSourceBeforeTagReplacement = $htmlSource;
// 1) Strip any inline code associated with the tag
// 2) Finally, strip the actual tag
$htmlSource = self::stripTagAndAnyInlineAssocCode( $scriptTag, $wpacuRegisteredScripts, $replaceWith, $htmlSource );
if ($htmlSource !== $htmlSourceBeforeTagReplacement) {
$scriptTagsStrippedNo ++;
}
}
// At least two tags have to be stripped from the group to consider doing the group replacement
// If the tags weren't replaced it's likely there were changes to their structure after they were cached for the group merging
if (count($filesSources) !== $scriptTagsStrippedNo) {
$htmlSource = $htmlSourceBeforeGroupReplacement;
}
}
}
}
// Only relevant if "Defer loading JavaScript combined files from <body>" in "Settings" - "Combine CSS & JS Files" - "Combine loaded JS (JavaScript) into fewer files"
// and there is at least one combined deferred tag
if (isset($finalCacheList['body']) && (! empty($finalCacheList['body'])) && Main::instance()->settings['combine_loaded_js_defer_body']) {
// CACHE RE-BUILT
if ($isDeferAppliedOnBodyCombineGroupNo > 0 && $domTag = ObjectCache::wpacu_cache_get('wpacu_html_dom_body_tag_for_js')) {
$strPart = "id='wpacu-combined-js-body-group-".$isDeferAppliedOnBodyCombineGroupNo."' ";
if (strpos($htmlSource, $strPart) === false) {
return $htmlSource; // something is funny, do not continue
}
list(,$htmlAfterFirstCombinedDeferScript) = explode($strPart, $htmlSource);
$htmlAfterFirstCombinedDeferScriptMaybeChanged = $htmlAfterFirstCombinedDeferScript;
$scriptTags = $domTag->getElementsByTagName('script');
} else {
// FROM THE CACHE
foreach ($finalCacheList['body'] as $bodyCombineGroupNo => $values) {
if (isset($values['extra_attributes']) && in_array('defer', $values['extra_attributes'])) {
$isDeferAppliedOnBodyCombineGroupNo = $bodyCombineGroupNo;
break;
}
}
if (! $isDeferAppliedOnBodyCombineGroupNo) {
// Not applicable to any combined group
return $htmlSource;
}
$strPart = 'id=\'wpacu-combined-js-body-group-'.$isDeferAppliedOnBodyCombineGroupNo.'\'';
$htmlAfterFirstCombinedDeferScriptMaybeChanged = false;
if (strpos($htmlSource, $strPart) !== false) {
list( , $htmlAfterFirstCombinedDeferScript ) = explode( $strPart, $htmlSource );
$htmlAfterFirstCombinedDeferScriptMaybeChanged = $htmlAfterFirstCombinedDeferScript;
}
// It means to combine took place for any reason (e.g. only one JS file loaded in the HEAD and one in the BODY)
if (! isset($htmlAfterFirstCombinedDeferScript)) {
return $htmlSource;
}
$domTag = Misc::initDOMDocument();
// Strip irrelevant tags to boost the speed of the parser (e.g. NOSCRIPT / SCRIPT(inline) / STYLE)
// Sometimes, inline CODE can be too large, and it takes extra time for loadHTML() to parse
$htmlSourceAlt = preg_replace( '@<script(| type=\'text/javascript\'| type="text/javascript")>.*?</script>@si', '', $htmlAfterFirstCombinedDeferScript );
$htmlSourceAlt = preg_replace( '@<(style|noscript)[^>]*?>.*?</\\1>@si', '', $htmlSourceAlt );
$htmlSourceAlt = preg_replace( '#<link([^<>]+)/?>#iU', '', $htmlSourceAlt );
if (Main::instance()->isFrontendEditView) {
$htmlSourceAlt = preg_replace( '@<form action="#wpacu_wrap_assets" method="post">.*?</form>@si', '', $htmlSourceAlt );
}
// No other SCRIPT left, stop here in this case
if (strpos($htmlSourceAlt, '<script') === false) {
return $htmlSource;
}
$domTag->loadHTML( $htmlSourceAlt );
$scriptTags = $domTag->getElementsByTagName('script');
}
if ( $scriptTags === null ) {
return $htmlSource;
}
foreach ($scriptTags as $tagObject) {
if (empty($tagObject->attributes)) { continue; }
$scriptAttributes = array();
foreach ( $tagObject->attributes as $attrObj ) {
$scriptAttributes[ $attrObj->nodeName ] = trim( $attrObj->nodeValue );
}
// No "src" attribute? Skip it (most likely an inline script tag)
if (! (isset($scriptAttributes['src']) && $scriptAttributes['src'])) {
continue;
}
// Skip it as "defer" is already set
if (isset($scriptAttributes['defer'])) {
continue;
}
// Has "src" attribute and "defer" is not applied? Add it
if ($htmlAfterFirstCombinedDeferScriptMaybeChanged !== false) {
$htmlAfterFirstCombinedDeferScriptMaybeChanged = trim( preg_replace(
'#\ssrc(\s+|)=(\s+|)(|"|\'|\s+)(' . preg_quote( $scriptAttributes['src'], '/' ) . ')(\3)#si',
' src=\3\4\3 defer=\'defer\'',
$htmlAfterFirstCombinedDeferScriptMaybeChanged
) );
}
}
if ($htmlAfterFirstCombinedDeferScriptMaybeChanged && $htmlAfterFirstCombinedDeferScriptMaybeChanged !== $htmlAfterFirstCombinedDeferScript) {
$htmlSource = str_replace($htmlAfterFirstCombinedDeferScript, $htmlAfterFirstCombinedDeferScriptMaybeChanged, $htmlSource);
}
}
libxml_clear_errors();
// Finally, return the HTML source
return $htmlSource;
}
/**
* @param $src
*
* @return bool
*/
public static function skipCombine($src, $handle = '')
{
// In case the handle was appended
if ($handle !== '' && in_array($handle, Main::instance()->skipAssets['scripts'])) {
return true;
}
$regExps = array(
'#/wp-content/bs-booster-cache/#'
);
if (Main::instance()->settings['combine_loaded_js_exceptions'] !== '') {
$loadedJsExceptionsPatterns = trim(Main::instance()->settings['combine_loaded_js_exceptions']);
if (strpos($loadedJsExceptionsPatterns, "\n")) {
// Multiple values (one per line)
foreach (explode("\n", $loadedJsExceptionsPatterns) as $loadedJsExceptionsPattern) {
$regExps[] = '#'.trim($loadedJsExceptionsPattern).'#';
}
} else {
// Only one value?
$regExps[] = '#'.trim($loadedJsExceptionsPatterns).'#';
}
}
// No exceptions set? Do not skip combination
if (empty($regExps)) {
return false;
}
foreach ($regExps as $regExp) {
$regExp = Misc::purifyRegexValue($regExp);
if ( @preg_match( $regExp, $src ) || ( strpos($src, $regExp) !== false ) ) {
// Skip combination
return true;
}
}
return false;
}
/**
* @param $localAssetsPaths
* @param $localAssetsExtra
* @param $docLocationScript
*
* @return array
*/
public static function maybeDoJsCombine($localAssetsPaths, $localAssetsExtra, $docLocationScript)
{
// Only combine if $shaOneCombinedUriPaths.js does not exist
// If "?ver" value changes on any of the assets or the asset list changes in any way
// then $shaOneCombinedUriPaths will change too and a new JS file will be generated and loaded
// Change $assetsContents as paths to fonts and images that are relative (e.g. ../, ../../) have to be updated
$uriToFinalJsFile = $localFinalJsFile = $finalJsContents = '';
foreach ($localAssetsPaths as $assetHref => $localAssetsPath) {
if ($jsContent = trim(FileSystem::fileGetContents($localAssetsPath))) {
// Does it have a source map? Strip it
if (strpos($jsContent, '//# sourceMappingURL=') !== false) {
$jsContent = OptimizeCommon::stripSourceMap($jsContent, 'js');
}
$pathToAssetDir = OptimizeCommon::getPathToAssetDir($assetHref);
$contentToAddToCombinedFile = '';
if (apply_filters('wpacu_print_info_comments_in_cached_assets', true)) {
$contentToAddToCombinedFile = '/*!' . str_replace( Misc::getWpRootDirPath(), '/', $localAssetsPath ) . "*/\n";
}
// This includes the extra from 'data' (CDATA added via wp_localize_script()) & 'before' as they are both printed BEFORE the SCRIPT tag
$contentToAddToCombinedFile .= self::maybeWrapBetweenTryCatch(self::appendToCombineJs('translations', $localAssetsExtra, $assetHref, $pathToAssetDir), $assetHref);
$contentToAddToCombinedFile .= self::maybeWrapBetweenTryCatch(self::appendToCombineJs('before', $localAssetsExtra, $assetHref, $pathToAssetDir), $assetHref);
$contentToAddToCombinedFile .= self::maybeWrapBetweenTryCatch(OptimizeJs::maybeDoJsFixes($jsContent, $pathToAssetDir . '/'), $assetHref) . "\n";
// This includes the inline 'after' the SCRIPT tag
$contentToAddToCombinedFile .= self::maybeWrapBetweenTryCatch(self::appendToCombineJs('after', $localAssetsExtra, $assetHref, $pathToAssetDir), $assetHref);
$finalJsContents .= $contentToAddToCombinedFile;
}
}
if ($finalJsContents !== '') {
$finalJsContents = trim($finalJsContents);
$shaOneForCombinedJs = sha1($finalJsContents);
$uriToFinalJsFile = $docLocationScript . '-' . $shaOneForCombinedJs . '.js';
$localFinalJsFile = WP_CONTENT_DIR . OptimizeJs::getRelPathJsCacheDir() . $uriToFinalJsFile;
if (! is_file($localFinalJsFile)) {
FileSystem::filePutContents( $localFinalJsFile, $finalJsContents );
}
}
return array(
'uri_final_js_file' => $uriToFinalJsFile,
'local_final_js_file' => $localFinalJsFile
);
}
/**
* @param $addItLocation
* @param $localAssetsExtra
* @param $assetHref
* @param $pathToAssetDir
*
* @return string
*/
public static function appendToCombineJs($addItLocation, $localAssetsExtra, $assetHref, $pathToAssetDir)
{
$extraContentToAppend = '';
$doJsMinifyInline = MinifyJs::isMinifyJsEnabled() && in_array(Main::instance()->settings['minify_loaded_js_for'], array('inline', 'all'));
if ($addItLocation === 'before') {
// [Before JS Content]
if (isset($localAssetsExtra[$assetHref]['data']) && ($dataValue = $localAssetsExtra[$assetHref]['data'])) {
$extraContentToAppend = '';
if (self::isInlineJsCombineable($dataValue) && trim($dataValue) !== '') {
$cData = $doJsMinifyInline ? MinifyJs::applyMinification( $dataValue ) : $dataValue;
$cData = OptimizeJs::maybeDoJsFixes( $cData, $pathToAssetDir . '/' );
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [inline: cdata] */' : '';
$extraContentToAppend .= $cData;
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [/inline: cdata] */' : '';
$extraContentToAppend .= "\n";
}
}
if (isset($localAssetsExtra[$assetHref]['before']) && ! empty($localAssetsExtra[$assetHref]['before'])) {
$inlineBeforeJsData = '';
foreach ($localAssetsExtra[$assetHref]['before'] as $beforeData) {
if (! is_bool($beforeData) && self::isInlineJsCombineable($beforeData)) {
$inlineBeforeJsData .= $beforeData . "\n";
}
}
if (trim($inlineBeforeJsData)) {
$inlineBeforeJsData = OptimizeJs::maybeAlterContentForInlineScriptTag( $inlineBeforeJsData, $doJsMinifyInline );
$inlineBeforeJsData = OptimizeJs::maybeDoJsFixes( $inlineBeforeJsData, $pathToAssetDir . '/' );
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [inline: before] */' : '';
$extraContentToAppend .= $inlineBeforeJsData;
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [/inline: before] */' : '';
$extraContentToAppend .= "\n";
}
}
// [/Before JS Content]
} elseif ($addItLocation === 'after') {
// [After JS Content]
if (isset($localAssetsExtra[$assetHref]['after']) && ! empty($localAssetsExtra[$assetHref]['after'])) {
$inlineAfterJsData = '';
foreach ($localAssetsExtra[$assetHref]['after'] as $afterData) {
if (! is_bool($afterData) && self::isInlineJsCombineable($afterData)) {
$inlineAfterJsData .= $afterData."\n";
}
}
if ( trim($inlineAfterJsData) ) {
$inlineAfterJsData = OptimizeJs::maybeAlterContentForInlineScriptTag( $inlineAfterJsData, $doJsMinifyInline );
$inlineAfterJsData = OptimizeJs::maybeDoJsFixes( $inlineAfterJsData, $pathToAssetDir . '/' );
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [inline: after] */' : '';
$extraContentToAppend .= $inlineAfterJsData;
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [/inline: after] */' : '';
$extraContentToAppend .= "\n";
}
}
// [/After JS Content]
} elseif ($addItLocation === 'translations' && isset($localAssetsExtra[$assetHref]['translations']) && $localAssetsExtra[$assetHref]['translations']) {
$inlineAfterJsData = OptimizeJs::maybeAlterContentForInlineScriptTag( $localAssetsExtra[$assetHref]['translations'], $doJsMinifyInline );
$inlineAfterJsData = OptimizeJs::maybeDoJsFixes( $inlineAfterJsData, $pathToAssetDir . '/' );
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [inline: translations] */' : '';
$extraContentToAppend .= $inlineAfterJsData;
$extraContentToAppend .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [/inline: translations] */' : '';
$extraContentToAppend .= "\n";
}
return $extraContentToAppend;
}
/**
* @param $jsCode
* @param $sourceUrl
*
* @return string
*/
public static function maybeWrapBetweenTryCatch($jsCode, $sourceUrl)
{
if ($jsCode && Main::instance()->settings['combine_loaded_js_try_catch']) {
return <<<JS
try {
{$jsCode}
} catch (err) {
console.log("Asset CleanUp - There is a JavaScript error related to the following source: {$sourceUrl} - Error: " + err.message);
}
JS;
}
return $jsCode;
}
/**
* @param $scriptTag
* @param $wpacuRegisteredScripts
* @param $replaceWith
* @param $htmlSource
*
* @return mixed
*/
public static function stripTagAndAnyInlineAssocCode($scriptTag, $wpacuRegisteredScripts, $replaceWith, $htmlSource)
{
if (OptimizeCommon::appendInlineCodeToCombineAssetType('js')) {
$scriptExtrasValue = OptimizeJs::getInlineAssociatedWithScriptHandle($scriptTag, $wpacuRegisteredScripts, 'tag', 'value');
$scriptExtraTranslationsValue = (isset($scriptExtrasValue['translations']) && $scriptExtrasValue['translations']) ? $scriptExtrasValue['translations'] : '';
$scriptExtraCdataValue = (isset($scriptExtrasValue['data']) && $scriptExtrasValue['data']) ? $scriptExtrasValue['data'] : '';
$scriptExtraBeforeValue = (isset($scriptExtrasValue['before']) && $scriptExtrasValue['before']) ? $scriptExtrasValue['before'] : '';
$scriptExtraAfterValue = (isset($scriptExtrasValue['after']) && $scriptExtrasValue['after']) ? $scriptExtrasValue['after'] : '';
$scriptExtrasHtml = OptimizeJs::getInlineAssociatedWithScriptHandle($scriptTag, $wpacuRegisteredScripts, 'tag', 'html');
preg_match_all('#data-wpacu-script-handle=([\'])' . '(.*)' . '(\1)#Usmi', $scriptTag, $outputMatches);
$scriptHandle = (isset($outputMatches[2][0]) && $outputMatches[2][0]) ? trim($outputMatches[2][0], '"\'') : '';
$scriptExtraTranslationsHtml = (isset($scriptExtrasHtml['translations']) && $scriptExtrasHtml['translations']) ? $scriptExtrasHtml['translations'] : '';
$scriptExtraCdataHtml = (isset($scriptExtrasHtml['data']) && $scriptExtrasHtml['data']) ? $scriptExtrasHtml['data'] : '';
$scriptExtraBeforeHtml = (isset($scriptExtrasHtml['before']) && $scriptExtrasHtml['before']) ? $scriptExtrasHtml['before'] : '';
$scriptExtraAfterHtml = (isset($scriptExtrasHtml['after']) && $scriptExtrasHtml['after']) ? $scriptExtrasHtml['after'] : '';
if ($scriptExtraTranslationsValue || $scriptExtraCdataValue || $scriptExtraBeforeValue || $scriptExtraAfterValue) {
if ( $scriptExtraCdataValue && self::isInlineJsCombineable($scriptExtraCdataValue) ) {
$htmlSource = str_replace($scriptExtraCdataHtml, '', $htmlSource );
}
if ($scriptExtraTranslationsValue) {
$repsBefore = array(
$scriptExtraTranslationsHtml => '',
str_replace( '<script ', '<script data-wpacu-script-handle=\'' . $scriptHandle . '\' ', $scriptExtraTranslationsHtml ) => '',
'>'."\n".$scriptExtraTranslationsValue."\n".'</script>' => '></script>',
$scriptExtraTranslationsValue."\n" => ''
);
$htmlSource = str_replace(array_keys($repsBefore), array_values($repsBefore), $htmlSource );
}
if ($scriptExtraBeforeValue && self::isInlineJsCombineable($scriptExtraBeforeValue)) {
$repsBefore = array(
$scriptExtraBeforeHtml => '',
str_replace( '<script ', '<script data-wpacu-script-handle=\'' . $scriptHandle . '\' ', $scriptExtraBeforeHtml ) => '',
'>'."\n".$scriptExtraBeforeValue."\n".'</script>' => '></script>',
$scriptExtraBeforeValue."\n" => ''
);
$htmlSource = str_replace(array_keys($repsBefore), array_values($repsBefore), $htmlSource );
}
if ($scriptExtraAfterValue && self::isInlineJsCombineable($scriptExtraAfterValue)) {
$repsBefore = array(
$scriptExtraAfterHtml => '',
str_replace( '<script ', '<script data-wpacu-script-handle=\'' . $scriptHandle . '\' ', $scriptExtraAfterHtml ) => '',
'>'."\n".$scriptExtraAfterValue."\n".'</script>' => '></script>',
$scriptExtraAfterValue."\n" => ''
);
$htmlSource = str_replace(array_keys($repsBefore), array_values($repsBefore), $htmlSource);
}
}
}
// Finally, strip/replace the tag
return str_replace( array($scriptTag."\n", $scriptTag), $replaceWith, $htmlSource );
}
/**
* This is to prevent certain inline JS to be appended to the combined JS files in order to avoid lots of disk space (sometimes a few GB) of JS combined files
*
* @param $jsInlineValue
*
* @return bool
*/
public static function isInlineJsCombineable($jsInlineValue)
{
// The common WordPress nonce
if (strpos($jsInlineValue, 'nonce') !== false) {
return false;
}
// WooCommerce Cart Fragments
if (strpos($jsInlineValue, 'wc_cart_hash_') !== false && strpos($jsInlineValue, 'cart_hash_key') !== false) {
return false;
}
if (substr(trim($jsInlineValue), 0, 1) === '{' && substr(trim($jsInlineValue), -1, 1) === '}') {
json_decode($jsInlineValue);
if (json_last_error() === JSON_ERROR_NONE) {
return false; // it's a JSON format (e.g. type="application/json" from "wordpress-popular-posts" plugin)
}
}
return true; // default
}
/**
* @return bool
*/
public static function proceedWithJsCombine()
{
// not on query string request (debugging purposes)
if ( isset($_REQUEST['wpacu_no_js_combine']) ) {
return false;
}
// No JS files are combined in the Dashboard
// Always in the front-end view
// Do not combine if there's a POST request as there could be assets loading conditionally
// that might not be needed when the page is accessed without POST, making the final JS file larger
if (! empty($_POST) || is_admin()) {
return false; // Do not combine
}
// Only clean request URIs allowed (with few exceptions)
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
// Exceptions
if (! OptimizeCommon::loadOptimizedAssetsIfQueryStrings()) {
return false;
}
}
if (! OptimizeCommon::doCombineIsRegularPage()) {
return false;
}
$pluginSettings = Main::instance()->settings;
if ($pluginSettings['test_mode'] && ! Menu::userCanManageAssets()) {
return false; // Do not combine anything if "Test Mode" is ON
}
if ($pluginSettings['combine_loaded_js'] === '') {
return false; // Do not combine
}
if (OptimizeJs::isOptimizeJsEnabledByOtherParty('if_enabled')) {
return false; // Do not combine (it's already enabled in other plugin)
}
// "Minify HTML" from WP Rocket is sometimes stripping combined SCRIPT tags
// Better uncombined then missing essential SCRIPT files
if (Misc::isWpRocketMinifyHtmlEnabled()) {
return false;
}
/*
if ( ($pluginSettings['combine_loaded_js'] === 'for_admin'
|| $pluginSettings['combine_loaded_js_for_admin_only'] == 1)
&& Menu::userCanManageAssets() ) {
return true; // Do combine
}
*/
// "Apply it only for guest visitors (default)" is set; Do not combine if the user is logged in
if ( $pluginSettings['combine_loaded_js_for'] === 'guests' && is_user_logged_in() ) {
return false;
}
if ( in_array($pluginSettings['combine_loaded_js'], array('for_all', 1)) ) {
return true; // Do combine
}
// Finally, return false as none of the checks above matched
return false;
}
}