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( '@@si', '', $htmlSource ); $htmlSourceAlt = preg_replace( '@<(style|noscript)[^>]*?>.*?\\1>@si', '', $htmlSourceAlt ); $htmlSourceAlt = preg_replace( '#]+)/?>#iU', '', $htmlSourceAlt ); if (Main::instance()->isFrontendEditView) { $htmlSourceAlt = preg_replace( '@
@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), '/* 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', '', 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 " 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( '@@si', '', $htmlAfterFirstCombinedDeferScript ); $htmlSourceAlt = preg_replace( '@<(style|noscript)[^>]*?>.*?\\1>@si', '', $htmlSourceAlt ); $htmlSourceAlt = preg_replace( '#]+)/?>#iU', '', $htmlSourceAlt ); if (Main::instance()->isFrontendEditView) { $htmlSourceAlt = preg_replace( '@@si', '', $htmlSourceAlt ); } // No other SCRIPT left, stop here in this case if (strpos($htmlSourceAlt, '' => '>', $scriptExtraTranslationsValue."\n" => '' ); $htmlSource = str_replace(array_keys($repsBefore), array_values($repsBefore), $htmlSource ); } if ($scriptExtraBeforeValue && self::isInlineJsCombineable($scriptExtraBeforeValue)) { $repsBefore = array( $scriptExtraBeforeHtml => '', str_replace( '' => '>', $scriptExtraBeforeValue."\n" => '' ); $htmlSource = str_replace(array_keys($repsBefore), array_values($repsBefore), $htmlSource ); } if ($scriptExtraAfterValue && self::isInlineJsCombineable($scriptExtraAfterValue)) { $repsBefore = array( $scriptExtraAfterHtml => '', str_replace( '' => '>', $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; } }