This commit is contained in:
2024-05-20 15:37:46 +03:00
commit 00b7dbd0b7
10404 changed files with 3285853 additions and 0 deletions

View File

@ -0,0 +1,627 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Menu;
use WpAssetCleanUp\FileSystem;
use WpAssetCleanUp\Misc;
use WpAssetCleanUp\ObjectCache;
use WpAssetCleanUp\Preloads;
/**
* Class CombineCss
* @package WpAssetCleanUp\OptimiseAssets
*/
class CombineCss
{
/**
* @var string
*/
public static $jsonStorageFile = 'css-combined{maybe-extra-info}.json';
/**
* @param $htmlSource
*
* @return mixed
*/
public static function doCombine($htmlSource)
{
if ( ! Misc::isDOMDocumentOn() ) {
return $htmlSource;
}
if ( ! self::proceedWithCssCombine() ) {
return $htmlSource;
}
global $wp_styles;
$wpacuRegisteredStyles = $wp_styles->registered;
$storageJsonContents = array();
$skipCache = false; // default
if (isset($_GET['wpacu_no_cache']) || (defined('WPACU_NO_CACHE') && WPACU_NO_CACHE === true)) {
$skipCache = true;
}
// If cache is not skipped, read the information from the cache as it's much faster
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
$storageJsonContents = OptimizeCommon::getAssetCachedData( self::$jsonStorageFile, OptimizeCss::getRelPathCssCacheDir(), 'css' );
}
// $uriToFinalCssFile will always be relative ONLY within WP_CONTENT_DIR . self::getRelPathCssCacheDir()
// which is usually "wp-content/cache/asset-cleanup/css/"
if ( $skipCache || empty($storageJsonContents) ) {
$storageJsonContentsToSave = array();
/*
* NO CACHING? Parse the DOM
*/
// Nothing in the database records or the retrieved cached file does not exist?
OptimizeCommon::clearAssetCachedData(self::$jsonStorageFile);
$storageJsonContents = array();
$domTag = OptimizeCommon::getDomLoadedTag($htmlSource, 'combineCss');
foreach (array('head', 'body') as $docLocationTag) {
$combinedUriPathsGroup = $localAssetsPathsGroup = $linkHrefsGroup = array();
$localAssetsExtraGroup = array();
$docLocationElements = $domTag->getElementsByTagName($docLocationTag)->item(0);
if ($docLocationElements === null) { continue; }
$xpath = new \DOMXpath($domTag);
$linkTags = $xpath->query('/html/'.$docLocationTag.'/link[@rel="stylesheet"] | /html/'.$docLocationTag.'/link[@rel="preload"]');
if ($linkTags === null) { continue; }
foreach ($linkTags as $tagObject) {
$linkAttributes = array();
foreach ($tagObject->attributes as $attrObj) { $linkAttributes[$attrObj->nodeName] = trim($attrObj->nodeValue); }
// Only rel="stylesheet" (with no rel="preload" associated with it) gets prepared for combining as links with rel="preload" (if any) are never combined into a standard render-blocking CSS file
// rel="preload" is there for a reason to make sure the CSS code is made available earlier prior to the one from rel="stylesheet" which is render-blocking
if (isset($linkAttributes['rel'], $linkAttributes['href']) && $linkAttributes['href']) {
$href = (string) $linkAttributes['href'];
if (self::skipCombine($linkAttributes['href'])) {
continue;
}
// e.g. for 'admin-bar' (keep it as standalone when critical CSS is used)
if (isset($linkAttributes['data-wpacu-skip-preload']) && has_filter('wpacu_critical_css')) {
continue;
}
// Check if the CSS file has any 'data-wpacu-skip' attribute; if it does, do not alter it
if (isset($linkAttributes['data-wpacu-skip']) || isset($scriptAttributes['data-wpacu-apply-media-query'])) {
continue;
}
// Separate each combined group by the "media" attribute; e.g. we don't want "all" and "print" mixed
$mediaValue = (array_key_exists('media', $linkAttributes) && $linkAttributes['media']) ? $linkAttributes['media'] : 'all';
// Check if there is any rel="preload" (Basic) connected to the rel="stylesheet"
// making sure the file is not added to the final CSS combined file
if (isset($linkAttributes['data-wpacu-style-handle']) &&
$linkAttributes['data-wpacu-style-handle'] &&
ObjectCache::wpacu_cache_get($linkAttributes['data-wpacu-style-handle'], 'wpacu_basic_preload_handles')) {
$mediaValue = 'wpacu_preload_basic_' . $mediaValue;
}
// Make the right reference for later use
if ($linkAttributes['rel'] === 'preload') {
if (isset($linkAttributes['data-wpacu-preload-css-basic'])) {
$mediaValue = 'wpacu_preload_basic_' . $mediaValue;
} else {
continue;
}
}
// Was it optimized and has the URL updated? Check the Source URL to determine if it should be skipped from combining
if (isset($linkAttributes['data-wpacu-link-rel-href-before']) && $linkAttributes['data-wpacu-link-rel-href-before'] && self::skipCombine($linkAttributes['data-wpacu-link-rel-href-before'])) {
continue;
}
// Avoid combining own plugin's CSS (irrelevant) as it takes extra useless space in the caching directory
if (isset($linkAttributes['id']) && $linkAttributes['id'] === WPACU_PLUGIN_ID.'-style-css') {
continue;
}
$localAssetPath = OptimizeCommon::getLocalAssetPath($href, 'css');
// It will skip external stylesheets (from a different domain)
if ( $localAssetPath ) {
$styleExtra = array();
if (isset($linkAttributes['data-wpacu-style-handle'], $wpacuRegisteredStyles[$linkAttributes['data-wpacu-style-handle']]->extra) && OptimizeCommon::appendInlineCodeToCombineAssetType('css')) {
$styleExtra = $wpacuRegisteredStyles[$linkAttributes['data-wpacu-style-handle']]->extra;
}
$sourceRelPath = OptimizeCommon::getSourceRelPath($href);
$alreadyAddedSourceRelPath = isset($combinedUriPathsGroup[$mediaValue]) && in_array($sourceRelPath, $combinedUriPathsGroup[$mediaValue]);
if (! $alreadyAddedSourceRelPath) {
$combinedUriPathsGroup[$mediaValue][] = $sourceRelPath;
}
$localAssetsPathsGroup[$mediaValue][$href] = $localAssetPath;
$alreadyAddedHref = isset($linkHrefsGroup[$mediaValue]) && in_array($href, $linkHrefsGroup[$mediaValue]);
if (! $alreadyAddedHref) {
$linkHrefsGroup[$mediaValue][] = $href;
}
$localAssetsExtraGroup[$mediaValue][$href] = $styleExtra;
}
}
}
// No Link Tags or only one tag in the combined group? Do not proceed with any combining
if ( empty( $combinedUriPathsGroup ) ) {
continue;
}
foreach ($combinedUriPathsGroup as $mediaValue => $combinedUriPaths) {
// There have to be at least two CSS files to create a combined CSS file
if (count($combinedUriPaths) < 2) {
continue;
}
$localAssetsPaths = $localAssetsPathsGroup[$mediaValue];
$linkHrefs = $linkHrefsGroup[$mediaValue];
$localAssetsExtra = array_filter($localAssetsExtraGroup[$mediaValue]);
$maybeDoCssCombine = self::maybeDoCssCombine(
$localAssetsPaths,
$linkHrefs,
$localAssetsExtra,
$docLocationTag
);
// Local path to combined CSS file
$localFinalCssFile = $maybeDoCssCombine['local_final_css_file'];
// URI (e.g. /wp-content/cache/asset-cleanup/[file-name-here.css]) to the combined CSS file
$uriToFinalCssFile = $maybeDoCssCombine['uri_final_css_file'];
// Any link hrefs removed perhaps if the file wasn't combined?
$linkHrefs = $maybeDoCssCombine['link_hrefs'];
if (is_file($localFinalCssFile)) {
$storageJsonContents[$docLocationTag][$mediaValue] = array(
'uri_to_final_css_file' => $uriToFinalCssFile,
'link_hrefs' => array_map(static function($href) {
return str_replace('{site_url}', '', OptimizeCommon::getSourceRelPath($href));
}, $linkHrefs)
);
$storageJsonContentsToSave[$docLocationTag][$mediaValue] = array(
'uri_to_final_css_file' => $uriToFinalCssFile,
'link_hrefs' => array_map(static function($href) {
return OptimizeCommon::getSourceRelPath($href);
}, $linkHrefs)
);
}
}
}
libxml_clear_errors();
OptimizeCommon::setAssetCachedData(
self::$jsonStorageFile,
OptimizeCss::getRelPathCssCacheDir(),
wp_json_encode($storageJsonContentsToSave)
);
}
$cdnUrls = OptimizeCommon::getAnyCdnUrls();
$cdnUrlForCss = isset($cdnUrls['css']) ? $cdnUrls['css'] : false;
if ( ! empty($storageJsonContents) ) {
foreach ($storageJsonContents as $docLocationTag => $mediaValues) {
$groupLocation = 1;
foreach ($mediaValues as $mediaValue => $storageJsonContentLocation) {
if (! isset($storageJsonContentLocation['link_hrefs'][0])) {
continue;
}
// Irrelevant to have only one CSS file in a combine CSS group
if (count($storageJsonContentLocation['link_hrefs']) < 2) {
continue;
}
$storageJsonContentLocation['link_hrefs'] = array_map(static function($href) {
return str_replace('{site_url}', '', $href);
}, $storageJsonContentLocation['link_hrefs']);
$finalTagUrl = OptimizeCommon::filterWpContentUrl($cdnUrlForCss) . OptimizeCss::getRelPathCssCacheDir() . $storageJsonContentLocation['uri_to_final_css_file'];
$finalCssTagAttrs = array();
if (strpos($mediaValue, 'wpacu_preload_basic_') === 0) {
// Put the right "media" value after cleaning the reference
$mediaValueClean = str_replace('wpacu_preload_basic_', '', $mediaValue);
// Basic Preload
$finalCssTag = <<<HTML
<link rel='stylesheet' data-wpacu-to-be-preloaded-basic='1' id='wpacu-combined-css-{$docLocationTag}-{$groupLocation}-preload-it-basic' href='{$finalTagUrl}' type='text/css' media='{$mediaValueClean}' />
HTML;
$finalCssTagRelPreload = <<<HTML
<link rel='preload' as='style' data-wpacu-preload-it-basic='1' id='wpacu-combined-css-{$docLocationTag}-{$groupLocation}-preload-it-basic' href='{$finalTagUrl}' type='text/css' media='{$mediaValueClean}' />
HTML;
$finalCssTagAttrs['rel'] = 'preload';
$finalCssTagAttrs['media'] = $mediaValueClean;
$htmlSource = str_replace(Preloads::DEL_STYLES_PRELOADS, $finalCssTagRelPreload."\n" . Preloads::DEL_STYLES_PRELOADS, $htmlSource);
} else {
// Render-blocking CSS
$finalCssTag = <<<HTML
<link rel='stylesheet' id='wpacu-combined-css-{$docLocationTag}-{$groupLocation}' href='{$finalTagUrl}' type='text/css' media='{$mediaValue}' />
HTML;
$finalCssTagAttrs['rel'] = 'stylesheet';
$finalCssTagAttrs['media'] = $mediaValue;
}
// In case one (e.g. usually a developer) needs to alter it
$finalCssTag = apply_filters(
'wpacu_combined_css_tag',
$finalCssTag,
array(
'attrs' => $finalCssTagAttrs,
'doc_location' => $docLocationTag,
'group_no' => $groupLocation,
'href' => $finalTagUrl
)
);
// Reference: https://stackoverflow.com/questions/2368539/php-replacing-multiple-spaces-with-a-single-space
$finalCssTag = preg_replace('!\s+!', ' ', $finalCssTag);
$htmlSourceBeforeAnyLinkTagReplacement = $htmlSource;
// Detect first LINK tag from the <$locationTag> and replace it with the final combined LINK tag
$firstLinkTag = OptimizeCss::getFirstLinkTag($storageJsonContentLocation['link_hrefs'][0], $htmlSource);
if ($firstLinkTag) {
// 1) Strip inline code before/after it (if any)
// 2) Finally, strip the actual tag
$htmlSource = self::stripTagAndAnyInlineAssocCode( $firstLinkTag, $wpacuRegisteredStyles, $finalCssTag, $htmlSource );
}
if ($htmlSource !== $htmlSourceBeforeAnyLinkTagReplacement) {
$htmlSource = self::stripJustCombinedLinkTags(
$storageJsonContentLocation['link_hrefs'],
$wpacuRegisteredStyles,
$htmlSource
); // Strip the combined files to avoid duplicate code
// There should be at least two replacements made AND all the tags should have been replaced
// Leave no room for errors, otherwise the page could end up with extra files loaded, leading to a slower website
if ($htmlSource === 'do_not_combine') {
$htmlSource = $htmlSourceBeforeAnyLinkTagReplacement;
} else {
$groupLocation++;
}
}
}
}
}
return $htmlSource;
}
/**
* @param $filesSources
* @param $wpacuRegisteredStyles
* @param $htmlSource
*
* @return mixed
*/
public static function stripJustCombinedLinkTags($filesSources, $wpacuRegisteredStyles, $htmlSource)
{
preg_match_all('#<link[^>]*(stylesheet|preload)[^>]*(>)#Umi', $htmlSource, $matchesSourcesFromTags, PREG_SET_ORDER);
$linkTagsStrippedNo = 0;
foreach ($matchesSourcesFromTags as $matchSourceFromTag) {
$matchedSourceFromTag = (isset($matchSourceFromTag[0]) && strip_tags($matchSourceFromTag[0]) === '') ? trim($matchSourceFromTag[0]) : '';
if (! $matchSourceFromTag) {
continue;
}
// The DOMDocument is already checked if it's enabled in doCombine()
$domTag = Misc::initDOMDocument();
$domTag->loadHTML($matchedSourceFromTag);
foreach ($domTag->getElementsByTagName('link') as $tagObject) {
if (empty($tagObject->attributes)) { continue; }
foreach ($tagObject->attributes as $tagAttrs) {
if ($tagAttrs->nodeName === 'href') {
$relNodeValue = trim(OptimizeCommon::getSourceRelPath($tagAttrs->nodeValue));
if (in_array($relNodeValue, $filesSources)) {
$htmlSourceBeforeLinkTagReplacement = $htmlSource;
// 1) Strip inline code before/after it (if any)
// 2) Finally, strip the actual tag
$htmlSource = self::stripTagAndAnyInlineAssocCode( $matchedSourceFromTag, $wpacuRegisteredStyles, '', $htmlSource );
if ($htmlSource !== $htmlSourceBeforeLinkTagReplacement) {
$linkTagsStrippedNo++;
}
}
}
}
}
libxml_clear_errors();
}
// Aren't all the LINK tags stripped? They should be, otherwise, do not proceed with the HTML alteration (no combining will take place)
// Minus the already combined tag
if (($linkTagsStrippedNo < 2) && (count($filesSources) !== $linkTagsStrippedNo)) {
return 'do_not_combine';
}
return $htmlSource;
}
/**
* @param $href
*
* @return bool
*/
public static function skipCombine($href)
{
$regExps = array(
'#/wp-content/bs-booster-cache/#'
);
if (Main::instance()->settings['combine_loaded_css_exceptions'] !== '') {
$loadedCssExceptionsPatterns = trim(Main::instance()->settings['combine_loaded_css_exceptions']);
if (strpos($loadedCssExceptionsPatterns, "\n")) {
// Multiple values (one per line)
foreach (explode("\n", $loadedCssExceptionsPatterns) as $loadedCssExceptionPattern) {
$regExps[] = '#'.trim($loadedCssExceptionPattern).'#';
}
} else {
// Only one value?
$regExps[] = '#'.trim($loadedCssExceptionsPatterns).'#';
}
}
// No exceptions set? Do not skip combination
if (empty($regExps)) {
return false;
}
foreach ($regExps as $regExp) {
$regExp = Misc::purifyRegexValue($regExp);
if ( @preg_match( $regExp, $href ) || ( strpos($href, $regExp) !== false ) ) {
// Skip combination
return true;
}
}
return false;
}
/**
* @param $localAssetsPaths
* @param $linkHrefs
* @param $localAssetsExtra
* @param $docLocationTag
*
* @return array
*/
public static function maybeDoCssCombine($localAssetsPaths, $linkHrefs, $localAssetsExtra, $docLocationTag)
{
// Only combine if $shaOneCombinedUriPaths.css 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 CSS file will be generated and loaded
// Change $finalCombinedCssContent as paths to fonts and images that are relative (e.g. ../, ../../) have to be updated + other optimization changes
$uriToFinalCssFile = $localFinalCssFile = $finalCombinedCssContent = '';
foreach ($localAssetsPaths as $assetHref => $localAssetsPath) {
if ($cssContent = trim(FileSystem::fileGetContents($localAssetsPath, 'combine_css_imports'))) {
$pathToAssetDir = OptimizeCommon::getPathToAssetDir($assetHref);
// Does it have a source map? Strip it
if (strpos($cssContent, '/*# sourceMappingURL=') !== false) {
$cssContent = OptimizeCommon::stripSourceMap($cssContent, 'css');
}
if (apply_filters('wpacu_print_info_comments_in_cached_assets', true)) {
$finalCombinedCssContent .= '/*!' . str_replace( Misc::getWpRootDirPath(), '/', $localAssetsPath ) . "*/\n";
}
$finalCombinedCssContent .= OptimizeCss::maybeFixCssContent($cssContent, $pathToAssetDir . '/') . "\n";
$finalCombinedCssContent = self::appendToCombineCss($localAssetsExtra, $assetHref, $pathToAssetDir, $finalCombinedCssContent);
}
}
// Move any @imports to top; This also strips any @imports to Google Fonts if the option is chosen
$finalCombinedCssContent = trim(OptimizeCss::importsUpdate($finalCombinedCssContent));
if (Main::instance()->settings['google_fonts_remove']) {
$finalCombinedCssContent = FontsGoogleRemove::cleanFontFaceReferences($finalCombinedCssContent);
}
$finalCombinedCssContent = apply_filters('wpacu_local_fonts_display_css_output', $finalCombinedCssContent, Main::instance()->settings['local_fonts_display']);
if ($finalCombinedCssContent) {
$finalCombinedCssContent = trim($finalCombinedCssContent);
$shaOneForCombinedCss = sha1($finalCombinedCssContent);
$uriToFinalCssFile = $docLocationTag . '-' .$shaOneForCombinedCss . '.css';
$localFinalCssFile = WP_CONTENT_DIR . OptimizeCss::getRelPathCssCacheDir() . $uriToFinalCssFile;
if (! is_file($localFinalCssFile)) {
FileSystem::filePutContents($localFinalCssFile, $finalCombinedCssContent);
}
}
return array(
'uri_final_css_file' => $uriToFinalCssFile,
'local_final_css_file' => $localFinalCssFile,
'link_hrefs' => $linkHrefs
);
}
/**
* @param $localAssetsExtra
* @param $assetHref
* @param $pathToAssetDir
* @param $finalAssetsContents
*
* @return string
*/
public static function appendToCombineCss($localAssetsExtra, $assetHref, $pathToAssetDir, $finalAssetsContents)
{
if (isset($localAssetsExtra[$assetHref]['after']) && ! empty($localAssetsExtra[$assetHref]['after'])) {
$afterCssContent = '';
foreach ($localAssetsExtra[$assetHref]['after'] as $afterData) {
if (! is_bool($afterData)) {
$afterCssContent .= $afterData."\n";
}
}
if (trim($afterCssContent)) {
if (MinifyCss::isMinifyCssEnabled() && in_array(Main::instance()->settings['minify_loaded_css_for'], array('inline', 'all'))) {
$afterCssContent = MinifyCss::applyMinification( $afterCssContent );
}
$afterCssContent = OptimizeCss::maybeFixCssContent( $afterCssContent, $pathToAssetDir . '/' );
$finalAssetsContents .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [inline: after] */' : '';
$finalAssetsContents .= $afterCssContent;
$finalAssetsContents .= apply_filters('wpacu_print_info_comments_in_cached_assets', true) ? '/* [/inline: after] */' : '';
$finalAssetsContents .= "\n";
}
}
return $finalAssetsContents;
}
/**
* The targeted LINK tag (which was enqueued and has a handle) is replaced with $replaceWith
* along with any inline content that was added after it via wp_add_inline_style()
*
* @param $targetedLinkTag
* @param $wpacuRegisteredStyles
* @param $replaceWith
* @param $htmlSource
*
* @return mixed
*/
public static function stripTagAndAnyInlineAssocCode($targetedLinkTag, $wpacuRegisteredStyles, $replaceWith, $htmlSource)
{
if (OptimizeCommon::appendInlineCodeToCombineAssetType('css')) {
$scriptExtrasHtml = OptimizeCss::getInlineAssociatedWithLinkHandle($targetedLinkTag, $wpacuRegisteredStyles, 'tag', 'html');
$scriptExtraAfterHtml = (isset($scriptExtrasHtml['after']) && $scriptExtrasHtml['after']) ? "\n".$scriptExtrasHtml['after'] : '';
$htmlSource = str_replace(
array(
$targetedLinkTag . $scriptExtraAfterHtml,
$targetedLinkTag . trim($scriptExtraAfterHtml)
),
$replaceWith,
$htmlSource
);
}
return str_replace(
array(
$targetedLinkTag."\n",
$targetedLinkTag
),
$replaceWith."\n",
$htmlSource
);
}
/**
* @return bool
*/
public static function proceedWithCssCombine()
{
// Not on query string request (debugging purposes)
if ( ! empty($_REQUEST) && array_key_exists('wpacu_no_css_combine', $_REQUEST) ) {
return false;
}
// No CSS 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 CSS file larger
if (! empty($_POST) || is_admin()) {
return false; // Do not combine
}
// Only clean request URIs allowed (with Exceptions)
// Exceptions
if ((strpos($_SERVER['REQUEST_URI'], '?') !== false) && ! 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 and the user is in guest mode (not logged-in)
}
if ($pluginSettings['combine_loaded_css'] === '') {
return false; // Do not combine
}
if (OptimizeCss::isOptimizeCssEnabledByOtherParty('if_enabled')) {
return false; // Do not combine (it's already enabled in other plugin)
}
// "Minify HTML" from WP Rocket is sometimes stripping combined LINK tags
// Better uncombined then missing essential CSS files
if (Misc::isWpRocketMinifyHtmlEnabled()) {
return false;
}
/*
// The option is no longer used since v1.1.7.3 (Pro) & v1.3.6.4 (Lite)
if ( ($pluginSettings['combine_loaded_css'] === 'for_admin'
|| $pluginSettings['combine_loaded_css_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_css_for'] === 'guests' && is_user_logged_in() ) {
return false;
}
if (in_array($pluginSettings['combine_loaded_css'], array('for_all', 1)) ) {
return true; // Do combine
}
// Finally, return false as none of the verification above matched
return false;
}
}

View File

@ -0,0 +1,514 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use MatthiasMullie\Minify\Minify;
use MatthiasMullie\PathConverter\ConverterInterface;
use MatthiasMullie\PathConverter\Converter;
/**
* Combine CSS Imports extended from CSS minifier
*
* Please report bugs on https://github.com/matthiasmullie/minify/issues
*
* @package Minify
* @author Matthias Mullie <minify@mullie.eu>
* @author Tijs Verkoyen <minify@verkoyen.eu>
* @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
* @license MIT License
*/
class CombineCssImports extends Minify
{
/**
* @var int maximum import size in kB
*/
protected $maxImportSize = 5;
/**
* @var string[] valid import extensions
*/
protected $importExtensions = array(
'gif' => 'data:image/gif',
'png' => 'data:image/png',
'jpe' => 'data:image/jpeg',
'jpg' => 'data:image/jpeg',
'jpeg' => 'data:image/jpeg',
'svg' => 'data:image/svg+xml',
'woff' => 'data:application/x-font-woff',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'xbm' => 'image/x-xbitmap',
);
/**
* Set the maximum size if files to be imported.
*
* Files larger than this size (in kB) will not be imported into the CSS.
* Importing files into the CSS as data-uri will save you some connections,
* but we should only import relatively small decorative images so that our
* CSS file doesn't get too bulky.
*
* @param int $size Size in kB
*/
public function setMaxImportSize($size)
{
$this->maxImportSize = $size;
}
/**
* Set the type of extensions to be imported into the CSS (to save network
* connections).
* Keys of the array should be the file extensions & respective values
* should be the data type.
*
* @param string[] $extensions Array of file extensions
*/
public function setImportExtensions(array $extensions)
{
$this->importExtensions = $extensions;
}
/**
* Move any import statements to the top.
*
* @param string $content Nearly finished CSS content
*
* @return string
*/
protected function moveImportsToTop($content)
{
if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
// remove from content
foreach ($matches[0] as $import) {
$content = str_replace($import, '', $content);
}
// add to top
$content = implode(';', $matches[2]).';'.trim($content, ';');
}
return $content;
}
/**
* Combine CSS from import statements.
*
* @import's will be loaded and their content merged into the original file,
* to save HTTP requests.
*
* @param string $source The file to combine imports for
* @param string $content The CSS content to combine imports for
* @param string[] $parents Parent paths, for circular reference checks
*
* @return string
*
*/
protected function combineImports($source, $content, $parents)
{
$importRegexes = array(
// @import url(xxx)
'/
# import statement
@import
# whitespace
\s+
# open url()
url\(
# (optional) open path enclosure
(?P<quotes>["\']?)
# fetch path
(?P<path>.+?)
# (optional) close path enclosure
(?P=quotes)
# close url()
\)
# (optional) trailing whitespace
\s*
# (optional) media statement(s)
(?P<media>[^;]*)
# (optional) trailing whitespace
\s*
# (optional) closing semi-colon
;?
/ix',
// @import 'xxx'
'/
# import statement
@import
# whitespace
\s+
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>.+?)
# close path enclosure
(?P=quotes)
# (optional) trailing whitespace
\s*
# (optional) media statement(s)
(?P<media>[^;]*)
# (optional) trailing whitespace
\s*
# (optional) closing semi-colon
;?
/ix',
);
// find all relative imports in css
$matches = array();
foreach ($importRegexes as $importRegex) {
if (preg_match_all($importRegex, $content, $regexMatches, PREG_SET_ORDER)) {
$matches = array_merge($matches, $regexMatches);
}
}
$search = array();
$replace = array();
// loop the matches
foreach ($matches as $match) {
// get the path for the file that will be imported
$importPath = dirname($source).'/'.$match['path'];
// only replace the import with the content if we can grab the
// content of the file
if (!$this->canImportByPath($match['path']) || !$this->canImportFile($importPath)) {
continue;
}
// check if current file was not imported previously in the same
// import chain.
if (in_array($importPath, $parents)) {
// No need to have and endless loop (the same file imported again and again)
$search[] = $match[0];
$replace[] = '';
continue;
}
// grab referenced file & optimize it (which may include importing
// yet other @import statements recursively)
$minifier = new static($importPath);
$minifier->setMaxImportSize($this->maxImportSize);
$minifier->setImportExtensions($this->importExtensions);
$importContent = $minifier->execute($source, $parents);
// check if this is only valid for certain media
if (!empty($match['media'])) {
$importContent = '@media '.$match['media'].'{'.$importContent.'}';
}
// add to replacement array
$search[] = $match[0];
$replace[] = $importContent;
}
// replace the import statements
return str_replace($search, $replace, $content);
}
/**
* @param $css
*
* @return mixed
*/
protected function alterImportsBetweenComments($css)
{
// RegEx Source: https://blog.ostermiller.org/finding-comments-in-source-code-using-regular-expressions/
preg_match_all('#/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/#', $css, $commentsMatches);
if (isset($commentsMatches[0]) && ! empty($commentsMatches[0])) {
foreach ($commentsMatches[0] as $commentMatch) {
if (strpos($commentMatch, '@import') === false) {
continue; // the comment needs to have @import
}
$newComment = str_replace('@import', '(wpacu)(at)import', $commentMatch);
$css = str_replace($commentMatch, $newComment, $css);
}
}
return $css;
}
/**
* Import files into the CSS, base64-sized.
*
* @url(image.jpg) images will be loaded and their content merged into the
* original file, to save HTTP requests.
*
* @param string $source The file to import files for
* @param string $content The CSS content to import files for
*
* @return string
*/
protected function importFiles($source, $content)
{
$regex = '/url\((["\']?)(.+?)\\1\)/i';
if ($this->importExtensions && preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
$search = array();
$replace = array();
// loop the matches
foreach ($matches as $match) {
$extension = substr(strrchr($match[2], '.'), 1);
if ($extension && !array_key_exists($extension, $this->importExtensions)) {
continue;
}
// get the path for the file that will be imported
$path = $match[2];
$path = dirname($source).'/'.$path;
// only replace the import with the content if we're able to get
// the content of the file, and it's relatively small
if ($this->canImportFile($path) && $this->canImportBySize($path)) {
// grab content && base64-ize
$importContent = $this->load($path);
$importContent = base64_encode($importContent);
// build replacement
$search[] = $match[0];
$replace[] = 'url('.$this->importExtensions[$extension].';base64,'.$importContent.')';
}
}
// replace the import statements
$content = str_replace($search, $replace, $content);
}
return $content;
}
/**
* Perform CSS optimizations.
*
* @param string[optional] $path Path to write the data to
* @param string[] $parents Parent paths, for circular reference checks
*
* @return string The minified data
*/
public function execute($path = null, $parents = array())
{
$content = '';
// loop CSS data (raw data and files)
foreach ($this->data as $source => $css) {
// Some developers might have wrapped @import between comments
// No import for those
$css = $this->alterImportsBetweenComments($css);
$source = is_int($source) ? '' : $source;
$parents = $source ? array_merge($parents, array($source)) : $parents;
$css = $this->combineImports($source, $css, $parents);
$css = $this->importFiles($source, $css);
/*
* If we'll save to a new path, we'll have to fix the relative paths
* to be relative no longer to the source file, but to the new path.
* If we don't write to a file, fall back to same path so no
* conversion happens (because we still want it to go through most
* of the move code, which also addresses url() & @import syntax...)
*/
$converter = $this->getPathConverter($source, $path ?: $source);
$css = $this->move($converter, $css);
// combine css
$content .= $css;
}
$content = $this->moveImportsToTop($content);
return $content;
}
/**
* Moving a css file should update all relative urls.
* Relative references (e.g. ../images/image.gif) in a certain css file,
* will have to be updated when a file is being saved at another location
* (e.g. ../../images/image.gif, if the new CSS file is 1 folder deeper).
*
* @param ConverterInterface $converter Relative path converter
* @param string $content The CSS content to update relative urls for
*
* @return string
*/
protected function move(ConverterInterface $converter, $content)
{
/*
* Relative path references will usually be enclosed by url(). @import
* is an exception, where url() is not necessary around the path (but is
* allowed).
* This *could* be 1 regular expression, where both regular expressions
* in this array are on different sides of a |. But we're using named
* patterns in both regexes, the same name on both regexes. This is only
* possible with a (?J) modifier, but that only works after a fairly
* recent PCRE version. That's why I'm doing 2 separate regular
* expressions & combining the matches after executing of both.
*/
$relativeRegexes = array(
// url(xxx)
'/
# open url()
url\(
\s*
# open path enclosure
(?P<quotes>["\'])?
# fetch path
(?P<path>.+?)
# close path enclosure
(?(quotes)(?P=quotes))
\s*
# close url()
\)
/ix',
// @import "xxx"
'/
# import statement
@import
# whitespace
\s+
# we don\'t have to check for @import url(), because the
# condition above will already catch these
# open path enclosure
(?P<quotes>["\'])
# fetch path
(?P<path>.+?)
# close path enclosure
(?P=quotes)
/ix',
);
// find all relative urls in css
$matches = array();
foreach ($relativeRegexes as $relativeRegex) {
if (preg_match_all($relativeRegex, $content, $regexMatches, PREG_SET_ORDER)) {
$matches = array_merge($matches, $regexMatches);
}
}
$search = array();
$replace = array();
// loop all urls
foreach ($matches as $match) {
// determine if it's an url() or an @import match
$type = (strpos($match[0], '@import') === 0 ? 'import' : 'url');
$url = $match['path'];
if ($this->canImportByPath($url)) {
// attempting to interpret GET-params makes no sense, so let's discard them for a while
$params = strrchr($url, '?');
$url = $params ? substr($url, 0, -strlen($params)) : $url;
// fix relative url
$url = $converter->convert($url);
// now that the path has been converted, re-apply GET-params
$url .= $params;
}
/*
* Urls with control characters above 0x7e should be quoted.
* According to Mozilla's parser, whitespace is only allowed at the
* end of unquoted urls.
* Urls with `)` (as could happen with data: uris) should also be
* quoted to avoid being confused for the url() closing parentheses.
* And urls with a # have also been reported to cause issues.
* Urls with quotes inside should also remain escaped.
*
* @see https://developer.mozilla.org/nl/docs/Web/CSS/url#The_url()_functional_notation
* @see https://hg.mozilla.org/mozilla-central/rev/14abca4e7378
* @see https://github.com/matthiasmullie/minify/issues/193
*/
$url = trim($url);
if (preg_match('/[\s\)\'"#\x{7f}-\x{9f}]/u', $url)) {
$url = $match['quotes'] . $url . $match['quotes'];
}
// build replacement
$search[] = $match[0];
if ($type === 'url') {
$replace[] = 'url('.$url.')';
} elseif ($type === 'import') {
$replace[] = '@import "'.$url.'"';
}
}
// replace urls
return str_replace($search, $replace, $content);
}
/**
* Check if file is small enough to be imported.
*
* @param string $path The path to the file
*
* @return bool
*/
protected function canImportBySize($path)
{
return ($size = @filesize($path)) && $size <= $this->maxImportSize * 1024;
}
/**
* Check if file a file can be imported, going by the path.
*
* @param string $path
*
* @return bool
*/
protected function canImportByPath($path)
{
return preg_match('/^(data:|https?:|\\/)/', $path) === 0;
}
/**
* Return a converter to update relative paths to be relative to the new
* destination.
*
* @param string $source
* @param string $target
*
* @return ConverterInterface
*/
protected function getPathConverter($source, $target)
{
return new Converter($source, $target);
}
}

View File

@ -0,0 +1,920 @@
<?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;
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
/**
* Class DynamicLoadedAssets
* @package WpAssetCleanUp
*/
class DynamicLoadedAssets
{
/**
* @param $from
* @param $value
*
* @return bool|mixed|string
*/
public static function getAssetContentFrom($from, $value)
{
$assetContent = '';
if ($from === 'simple-custom-css') {
/*
* Special Case: "Simple Custom CSS" Plugin
*
* /?sccss=1
*
* As it is (no minification or optimization), it adds extra load time to the page
* as the CSS is read via PHP and all the WP environment is loading
*/
if (! $assetContent = self::getSimpleCustomCss()) {
return false;
}
}
if ($from === 'dynamic') { // /? .php? etc.
if (! OptimizeCommon::isSourceFromSameHost($value->src)) {
return array();
}
$response = wp_remote_get(
$value->src
);
if (wp_remote_retrieve_response_code($response) !== 200) {
return false;
}
if (! $assetContent = wp_remote_retrieve_body($response)) {
return false;
}
}
return $assetContent;
}
/**
* "Simple Custom CSS" (better retrieval, especially for localhost and password-protected sites)
*
* @return string
*/
public static function getSimpleCustomCss()
{
$sccssOptions = get_option('sccss_settings');
$sccssRawContent = isset($sccssOptions['sccss-content']) ? $sccssOptions['sccss-content'] : '';
$cssContent = wp_kses($sccssRawContent, array('\'', '\"'));
$cssContent = str_replace('&gt;', '>', $cssContent);
return trim($cssContent);
}
}

View File

@ -0,0 +1,764 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Misc;
use WpAssetCleanUp\Plugin;
/**
* Class FontsGoogle
* @package WpAssetCleanUp\OptimiseAssets
*/
class FontsGoogle
{
/**
* @var string
*/
public static $containsStr = '//fonts.googleapis.com/';
/**
* @var string
*/
public static $matchesStr = '//fonts.googleapis.com/(css|icon)\?';
/**
*
*/
const NOSCRIPT_WEB_FONT_LOADER = '<span style="display: none;" data-name=wpacu-delimiter content="ASSET CLEANUP NOSCRIPT WEB FONT LOADER"></span>';
/**
*
*/
const COMBINED_LINK_DEL = '<span style="display: none;" data-name=wpacu-delimiter content="ASSET CLEANUP COMBINED LINK LOCATION"></span>';
/**
*
*/
public function init()
{
if (self::preventAnyChange()) {
return;
}
add_filter('wp_resource_hints', array($this, 'resourceHints'), PHP_INT_MAX, 2);
add_action('wp_head', array($this, 'preloadFontFiles'), 1);
add_action('wp_footer', static function() {
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive() || Main::instance()->settings['google_fonts_remove'] ) {
return;
}
echo self::NOSCRIPT_WEB_FONT_LOADER;
}, PHP_INT_MAX);
add_filter('wpacu_html_source_after_optimization', static function($htmlSource) {
// Is the mark still there and wasn't replaced? Strip it
return str_replace(FontsGoogle::NOSCRIPT_WEB_FONT_LOADER, '', $htmlSource);
});
add_action('init', function() {
// don't apply any changes if not in the front-end view (e.g. Dashboard view)
// or test mode is enabled and a guest user is accessing the page
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive() || Main::instance()->settings['google_fonts_remove'] ) {
return;
}
add_filter('style_loader_src', array($this, 'alterGoogleFontLink'));
}, 20);
}
/**
* @param $urls
* @param $relationType
*
* @return array
*/
public function resourceHints($urls, $relationType)
{
// don't apply any changes if not in the front-end view (e.g. Dashboard view)
// or test mode is enabled and a guest user is accessing the page
if (is_admin() || Main::isTestModeActive() || Plugin::preventAnyFrontendOptimization()) {
return $urls;
}
// Are the Google Fonts removed? Do not add it and strip any existing ones
if (! empty($urls) && Main::instance()->settings['google_fonts_remove']) {
foreach ($urls as $urlKey => $urlValue) {
if (is_string($urlValue) && ((stripos($urlValue, 'fonts.googleapis.com') !== false) || (stripos($urlValue, 'fonts.gstatic.com') !== false))) {
unset($urls[$urlKey]);
}
}
return $urls; // Finally, return the list after any removals
}
// Google Fonts "preconnect"
if ('preconnect' === $relationType
&& ! Main::instance()->settings['google_fonts_remove'] // "Remove Google Fonts" has to be turned off
&& Main::instance()->settings['google_fonts_preconnect']) { // Needs to be enabled within "Plugin Usage Preferences" in "Settings"
$urls[] = array(
'href' => 'https://fonts.gstatic.com/',
'crossorigin'
);
}
return $urls;
}
/**
*
*/
public function preloadFontFiles()
{
// don't apply any changes if not in the front-end view (e.g. Dashboard view)
// or test mode is enabled and a guest user is accessing the page
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive() ) {
return;
}
if (! $preloadFontFiles = trim(Main::instance()->settings['google_fonts_preload_files'])) {
return;
}
$preloadFontFilesArray = array();
if (strpos($preloadFontFiles, "\n") !== false) {
foreach (explode("\n", $preloadFontFiles) as $preloadFontFile) {
$preloadFontFile = trim($preloadFontFile);
if (! $preloadFontFile) {
continue;
}
$preloadFontFilesArray[] = $preloadFontFile;
}
} else {
$preloadFontFilesArray[] = $preloadFontFiles;
}
$preloadFontFilesArray = array_unique($preloadFontFilesArray);
$preloadFontFilesOutput = '';
// Finally, go through the list
foreach ($preloadFontFilesArray as $preloadFontFile) {
$preloadFontFilesOutput .= '<link rel="preload" as="font" href="'.esc_attr($preloadFontFile).'" data-wpacu-preload-font="1" crossorigin>'."\n";
}
echo apply_filters('wpacu_preload_google_font_files_output', $preloadFontFilesOutput);
}
/**
* @param $htmlSource
*
* @return false|mixed|string|void
*/
public static function alterHtmlSource($htmlSource)
{
// don't apply any changes if not in the front-end view (e.g. Dashboard view)
// or test mode is enabled and a guest user is accessing the page
// or an AMP page is accessed
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive()) {
return $htmlSource;
}
/*
* Remove Google Fonts? Stop here as optimization is no longer relevant
*/
if (Main::instance()->settings['google_fonts_remove']) {
return FontsGoogleRemove::cleanHtmlSource($htmlSource);
}
/*
* Optimize Google Fonts
*/
if (stripos($htmlSource, self::$containsStr) !== false) {
// Cleaner HTML Source
$altHtmlSource = preg_replace( '@<(script|style|noscript)[^>]*?>.*?</\\1>@si', '', $htmlSource ); // strip irrelevant tags for the collection
$altHtmlSource = preg_replace( '/<!--[^>]*' . preg_quote( self::$containsStr, '/' ) . '.*?-->/', '', $altHtmlSource ); // strip any comments containing the string
// Get all valid LINKs that have the $string within them
preg_match_all( '#<link[^>]*' . self::$matchesStr . '.*(>)#Usmi', $altHtmlSource, $matchesFromLinkTags, PREG_SET_ORDER );
// Needs to match at least one to carry on with the replacements
if ( isset( $matchesFromLinkTags[0] ) && ! empty( $matchesFromLinkTags[0] ) ) {
if ( Main::instance()->settings['google_fonts_combine'] ) {
/*
* "Combine Google Fonts" IS enabled
*/
$finalCombinableLinks = $preloadedLinks = array();
foreach ( $matchesFromLinkTags as $linkTagArray ) {
$linkTag = $finalLinkTag = trim( trim( $linkTagArray[0], '"\'' ) );
// Extra checks to make sure it's a valid LINK tag
if ( ( strpos( $linkTag, "'" ) !== false && ( substr_count( $linkTag, "'" ) % 2 ) )
|| ( strpos( $linkTag, '"' ) !== false && ( substr_count( $linkTag, '"' ) % 2 ) )
|| ( trim( strip_tags( $linkTag ) ) !== '' ) ) {
continue;
}
// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not continue and leave it as it is (non-combined)
if ( preg_match( '#data-wpacu-skip([=>/ ])#i', $linkTag ) ) {
continue;
}
$finalLinkHref = $linkHrefOriginal = Misc::getValueFromTag($linkTag);
// [START] Remove invalid requests with no font family
$urlParse = parse_url( str_replace( '&amp;', '&', $linkHrefOriginal ), PHP_URL_QUERY );
parse_str( $urlParse, $qStr );
if ( isset( $qStr['family'] ) && ! $qStr['family'] ) {
$htmlSource = str_replace( $linkTag, '', $htmlSource );
continue;
}
// [END] Remove invalid requests with no font family
// If anything is set apart from '[none set]', proceed
if ( Main::instance()->settings['google_fonts_display'] ) {
$finalLinkHref = self::alterGoogleFontLink( $linkHrefOriginal );
if ( $finalLinkHref !== $linkHrefOriginal ) {
$finalLinkTag = str_replace( $linkHrefOriginal, $finalLinkHref, $linkTag );
// Finally, alter the HTML source
$htmlSource = str_replace( $linkTag, $finalLinkTag, $htmlSource );
}
}
if ( preg_match( '/rel=(["\'])preload(["\'])/i', $finalLinkTag )
|| strpos( $finalLinkTag, 'data-wpacu-to-be-preloaded-basic' ) ) {
$preloadedLinks[] = $finalLinkHref;
}
$finalCombinableLinks[] = array( 'href' => $finalLinkHref, 'tag' => $finalLinkTag );
}
$preloadedLinks = array_unique( $preloadedLinks );
// Remove data for preloaded LINKs
if ( ! empty( $preloadedLinks ) ) {
foreach ( $finalCombinableLinks as $fclIndex => $combinableLinkData ) {
if ( in_array( $combinableLinkData['href'], $preloadedLinks ) ) {
unset( $finalCombinableLinks[ $fclIndex ] );
}
}
}
$finalCombinableLinks = array_values( $finalCombinableLinks );
// Only proceed with the optimization/combine if there's obviously at least 2 combinable URL requests to Google Fonts
// OR the loading type is different from render-blocking
if ( Main::instance()->settings['google_fonts_combine_type'] || count( $finalCombinableLinks ) > 1 ) {
$htmlSource = self::combineGoogleFontLinks( $finalCombinableLinks, $htmlSource );
}
} elseif (Main::instance()->settings['google_fonts_display']) {
/*
* "Combine Google Fonts" IS NOT enabled
* Go through the links and apply any "font-display"
*/
foreach ( $matchesFromLinkTags as $linkTagArray ) {
$linkTag = trim( trim( $linkTagArray[0], '"\'' ) );
// Extra checks to make sure it's a valid LINK tag
if ( ( strpos( $linkTag, "'" ) !== false && ( substr_count( $linkTag, "'" ) % 2 ) )
|| ( strpos( $linkTag, '"' ) !== false && ( substr_count( $linkTag, '"' ) % 2 ) )
|| ( trim( strip_tags( $linkTag ) ) !== '' ) ) {
continue;
}
// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not continue and leave it as it is (non-altered)
if ( preg_match( '#data-wpacu-skip([=>/ ])#i', $linkTag ) ) {
continue;
}
$linkHrefOriginal = Misc::getValueFromTag($linkTag);
// [START] Remove invalid requests with no font family
$urlParse = parse_url( str_replace( '&amp;', '&', $linkHrefOriginal ), PHP_URL_QUERY );
parse_str( $urlParse, $qStr );
if ( isset( $qStr['family'] ) && ! $qStr['family'] ) {
$htmlSource = str_replace( $linkTag, '', $htmlSource );
continue;
}
// [END] Remove invalid requests with no font family
// If anything is set apart from '[none set]', proceed
$newLinkHref = self::alterGoogleFontLink( $linkHrefOriginal );
if ( $newLinkHref !== $linkHrefOriginal ) {
$finalLinkTag = str_replace( $linkHrefOriginal, $newLinkHref, $linkTag );
// Finally, alter the HTML source
$htmlSource = str_replace( $linkTag, $finalLinkTag, $htmlSource );
}
}
}
}
}
// "font-display: swap;" if enabled
$htmlSource = self::alterGoogleFontUrlFromInlineStyleTags($htmlSource);
// Clear any traces
return str_replace(self::NOSCRIPT_WEB_FONT_LOADER, '', $htmlSource);
}
/**
* @param $linkHrefOriginal
* @param bool $escHtml
* @param $alterFor
*
* @return string
*/
public static function alterGoogleFontLink($linkHrefOriginal, $escHtml = true, $alterFor = 'css')
{
$isInVar = false; // The link is inside a variable with a JSON format
// Some special filtering here as some hosting environments (at least staging) behave funny with // inside SCRIPT tags
if ($alterFor === 'js') {
$containsStrNoSlashes = str_replace('/', '', self::$containsStr);
$conditionOne = stripos($linkHrefOriginal, $containsStrNoSlashes) === false;
if (strpos($linkHrefOriginal, '\/') !== false) {
$isInVar = true;
}
} else { // css (default)
$conditionOne = stripos($linkHrefOriginal, self::$containsStr) === false;
}
// Do not continue if it doesn't contain the right string, or it contains 'display=' or it does not contain 'family=' or there is no value set for "font-display"
if ($conditionOne ||
stripos($linkHrefOriginal, 'display=') !== false ||
stripos($linkHrefOriginal, 'family=') === false ||
! Main::instance()->settings['google_fonts_display']) {
// Return original source
return $linkHrefOriginal;
}
$altLinkHref = str_replace('&#038;', '&', $linkHrefOriginal);
if ($isInVar) {
$altLinkHref = str_replace('\/', '/', $altLinkHref);
}
$urlQuery = parse_url($altLinkHref, PHP_URL_QUERY);
parse_str($urlQuery, $outputStr);
// Is there no "display" or there is, but it has an empty value? Append the one we have in the "Settings" - "Google Fonts"
if ( ! isset($outputStr['display']) || (isset($outputStr['display']) && $outputStr['display'] === '') ) {
$outputStr['display'] = Main::instance()->settings['google_fonts_display'];
list($linkHrefFirstPart) = explode('?', $linkHrefOriginal);
// Returned the updated source with the 'display' parameter appended to it
$afterQuestionMark = http_build_query($outputStr);
if ($escHtml) {
$afterQuestionMark = esc_attr($afterQuestionMark);
}
return $linkHrefFirstPart . '?' . $afterQuestionMark;
}
// Return original source
return $linkHrefOriginal;
}
/**
* @param $htmlSource
*
* @return mixed
*/
public static function alterGoogleFontUrlFromInlineStyleTags($htmlSource)
{
if (! preg_match('/@import(\s+)url\(/i', $htmlSource)) {
return $htmlSource;
}
preg_match_all('#<\s*?style\b[^>]*>(.*?)</style\b[^>]*>#s', $htmlSource, $styleMatches, PREG_SET_ORDER);
if (empty($styleMatches)) {
return $htmlSource;
}
// Go through each STYLE tag
foreach ($styleMatches as $styleInlineArray) {
list($styleInlineTag, $styleInlineContent) = $styleInlineArray;
// Check if the STYLE tag has any 'data-wpacu-skip' attribute; if it does, do not continue
if (preg_match('#data-wpacu-skip([=>/ ])#i', $styleInlineTag)) {
continue;
}
// Is the content relevant?
if (! preg_match('/@import(\s+|)(url|\(|\'|")/i', $styleInlineContent)
|| stripos($styleInlineContent, 'fonts.googleapis.com') === false) {
continue;
}
// Do any alteration to the URL of the Google Font
$newCssOutput = self::alterGoogleFontUrlFromCssContent($styleInlineTag);
$htmlSource = str_replace($styleInlineTag, $newCssOutput, $htmlSource);
}
return $htmlSource;
}
/**
* @param $cssContent
*
* @return mixed
*/
public static function alterGoogleFontUrlFromCssContent($cssContent)
{
if (stripos($cssContent, 'fonts.googleapis.com') === false || ! Main::instance()->settings['google_fonts_display']) {
return $cssContent;
}
$regExps = array('/@import(\s+)url\((.*?)\)(|\s+)\;/i', '/@import(\s+|)(\(|\'|")(.*?)(\'|"|\))\;/i');
$newCssOutput = $cssContent;
foreach ($regExps as $regExpIndex => $regExpPattern) {
preg_match_all($regExpPattern, $cssContent, $matchesFromInlineCode, PREG_SET_ORDER);
if (! empty($matchesFromInlineCode)) {
foreach ($matchesFromInlineCode as $matchesFromInlineCodeArray) {
$cssImportRule = $matchesFromInlineCodeArray[0];
if ($regExpIndex === 0) {
$googleApisUrl = trim($matchesFromInlineCodeArray[2], '"\' ');
} else {
$googleApisUrl = trim($matchesFromInlineCodeArray[3], '"\' ');
}
// It has to be a Google Fonts API link
if (stripos($googleApisUrl, 'fonts.googleapis.com') === false) {
continue;
}
$newGoogleApisUrl = self::alterGoogleFontLink($googleApisUrl, false);
if ($newGoogleApisUrl !== $googleApisUrl) {
$newCssImportRule = str_replace($googleApisUrl, $newGoogleApisUrl, $cssImportRule);
$newCssOutput = str_replace($cssImportRule, $newCssImportRule, $newCssOutput);
}
}
}
}
return $newCssOutput;
}
/**
* @param $jsContent
*
* @return mixed
*/
public static function alterGoogleFontUrlFromJsContent($jsContent)
{
if (stripos($jsContent, 'fonts.googleapis.com') === false) {
return $jsContent;
}
$newJsOutput = $jsContent;
preg_match_all('#fonts.googleapis.com(.*?)(["\'])#si', $jsContent, $matchesFromJsCode);
if (isset($matchesFromJsCode[0]) && ! empty($matchesFromJsCode)) {
foreach ($matchesFromJsCode[0] as $match) {
$matchRule = $match;
$googleApisUrl = trim($match, '"\' ');
$newGoogleApisUrl = self::alterGoogleFontLink($googleApisUrl, false, 'js');
if ($newGoogleApisUrl !== $googleApisUrl) {
$newJsMatchOutput = str_replace($googleApisUrl, $newGoogleApisUrl, $matchRule);
$newJsOutput = str_replace($matchRule, $newJsMatchOutput, $newJsOutput);
}
}
}
// Look for any "WebFontConfig = { google: { families: ['font-one', 'font-two'] } }" patterns
if ( stripos( $jsContent, 'WebFontConfig' ) !== false
&& preg_match_all( '#WebFontConfig(.*?)google(\s+|):(\s+|){(\s+|)families(\s+|):(?<families>.*?)]#s', $jsContent, $webFontConfigMatches )
&& isset( $webFontConfigMatches['families'] ) && ! empty( $webFontConfigMatches['families'] )
) {
foreach ($webFontConfigMatches['families'] as $webFontConfigKey => $webFontConfigMatch) {
$originalWholeMatch = $webFontConfigMatches[0][$webFontConfigKey];
$familiesMatchOutput = trim($webFontConfigMatch);
// NO match or existing "display" parameter was found? Do not continue
if (! $familiesMatchOutput || strpos($familiesMatchOutput, 'display=')) {
continue;
}
// Alter the matched string
$familiesNewOutput = preg_replace('/([\'"])$/', '&display='.Main::instance()->settings['google_fonts_display'].'\\1', $familiesMatchOutput);
$newWebFontConfigOutput = str_replace($familiesMatchOutput, $familiesNewOutput, $originalWholeMatch);
// Finally, do the replacement
$newJsOutput = str_replace($originalWholeMatch, $newWebFontConfigOutput, $newJsOutput);
}
}
return $newJsOutput;
}
/**
* @param $finalLinks
* @param $htmlSource
*
* @return false|mixed|string|void
*/
public static function combineGoogleFontLinks($finalLinks, $htmlSource)
{
$fontsArray = array();
foreach ($finalLinks as $finalLinkIndex => $finalLinkData) {
$finalLinkHref = $finalLinkData['href'];
$finalLinkHref = str_replace('&#038;', '&', $finalLinkHref);
$queries = parse_url($finalLinkHref, PHP_URL_QUERY);
parse_str($queries, $fontQueries);
if (! array_key_exists('family', $fontQueries) || array_key_exists('text', $fontQueries)) {
continue;
}
// Strip the existing tag, leave a mark where the final combined LINK will be placed
$stripTagWith = ($finalLinkIndex === 0) ? self::COMBINED_LINK_DEL : '';
$finalLinkTag = $finalLinkData['tag'];
$htmlSource = str_ireplace(array($finalLinkTag."\n", $finalLinkTag), $stripTagWith, $htmlSource);
$family = trim($fontQueries['family']);
$family = trim($family, '|');
if (! $family) {
continue;
}
if (strpos($family, '|') !== false) {
// More than one family per request?
foreach (explode('|', $family) as $familyOne) {
if (strpos($familyOne, ':') !== false) {
// They have types
list ($familyRaw, $familyTypes) = explode(':', $familyOne);
$fontsArray['families'][$familyRaw]['types'] = self::buildSortTypesList($familyTypes);
} else {
// They do not have types
$familyRaw = $familyOne;
$fontsArray['families'][$familyRaw]['types'] = false;
}
}
} elseif (strpos($family, ':') !== false) {
list ($familyRaw, $familyTypes) = explode(':', $family);
$fontsArray['families'][$familyRaw]['types'] = self::buildSortTypesList($familyTypes);
} else {
$familyRaw = $family;
$fontsArray['families'][$familyRaw]['types'] = false;
}
if (array_key_exists('subset', $fontQueries)) {
// More than one subset per request?
if (strpos($fontQueries['subset'], ',') !== false) {
$multipleSubsets = explode(',', trim($fontQueries['subset'], ','));
foreach ($multipleSubsets as $subset) {
$fontsArray['subsets'][] = trim($subset);
}
} else {
// Only one subset
$fontsArray['subsets'][] = $fontQueries['subset'];
}
}
if (array_key_exists('effect', $fontQueries)) {
// More than one subset per request?
if (strpos($fontQueries['effect'], '|') !== false) {
$multipleSubsets = explode('|', trim($fontQueries['effect'], '|'));
foreach ($multipleSubsets as $subset) {
$fontsArray['effects'][] = trim($subset);
}
} else {
// Only one subset
$fontsArray['effects'][] = $fontQueries['effect'];
}
}
}
if (! empty($fontsArray)) {
$finalCombinedParameters = '';
ksort($fontsArray['families']);
// Families
foreach ($fontsArray['families'] as $familyRaw => $fontValues) {
$finalCombinedParameters .= str_replace(' ', '+', $familyRaw);
// Any types? e.g. 400, 400italic, bold, etc.
if (isset($fontValues['types']) && $fontValues['types'] !== false) {
$finalCombinedParameters .= ':' . $fontValues['types'];
}
$finalCombinedParameters .= '|';
}
$finalCombinedParameters = trim($finalCombinedParameters, '|');
// Subsets
if (isset($fontsArray['subsets']) && ! empty($fontsArray['subsets'])) {
sort($fontsArray['subsets']);
$finalCombinedParameters .= '&subset=' . implode(',', array_unique($fontsArray['subsets']));
}
// Effects
if (isset($fontsArray['effects']) && ! empty($fontsArray['effects'])) {
sort($fontsArray['effects']);
$finalCombinedParameters .= '&effect=' . implode('|', array_unique($fontsArray['effects']));
}
if ($fontDisplay = Main::instance()->settings['google_fonts_display']) {
$finalCombinedParameters .= '&display=' . $fontDisplay;
}
$finalCombinedParameters = esc_attr($finalCombinedParameters);
// This is needed for both render-blocking and async (within NOSCRIPT tag as a fallback)
$finalCombinedLink = <<<LINK
<link rel='stylesheet' id='wpacu-combined-google-fonts-css' href='https://fonts.googleapis.com/css?family={$finalCombinedParameters}' type='text/css' media='all' />
LINK;
/*
* Loading Type: Render-Blocking (Default)
*/
if (! Main::instance()->settings['google_fonts_combine_type']) {
$finalCombinedLink .= "\n";
$htmlSource = str_replace(self::COMBINED_LINK_DEL, apply_filters('wpacu_combined_google_fonts_link_tag', $finalCombinedLink), $htmlSource);
}
/*
* Loading Type: Asynchronous via LINK preload with fallback
*/
if (Main::instance()->settings['google_fonts_combine_type'] === 'async_preload') {
$finalPreloadCombinedLink = <<<LINK
<link rel='preload' as="style" onload="this.onload=null;this.rel='stylesheet'" data-wpacu-preload-it-async='1' id='wpacu-combined-google-fonts-css-async-preload' href='https://fonts.googleapis.com/css?family={$finalCombinedParameters}' type='text/css' media='all' />
LINK;
$finalPreloadCombinedLink .= "\n".Misc::preloadAsyncCssFallbackOutput();
$htmlSource = str_replace(self::COMBINED_LINK_DEL, apply_filters('wpacu_combined_google_fonts_async_preload_link_tag', $finalPreloadCombinedLink), $htmlSource);
}
/*
* Loading Type: Asynchronous via Web Font Loader (webfont.js) with fallback
*/
if (Main::instance()->settings['google_fonts_combine_type'] === 'async') { // Async via Web Font Loader
$subSetsStr = '';
if (isset($fontsArray['subsets']) && ! empty($fontsArray['subsets'])) {
sort($fontsArray['subsets']);
$subSetsStr = implode(',', array_unique($fontsArray['subsets']));
}
$wfConfigGoogleFamilies = array();
// Families
$iCount = 0;
foreach ($fontsArray['families'] as $familyRaw => $fontValues) {
$wfConfigGoogleFamily = str_replace(' ', '+', $familyRaw);
// Any types? e.g. 400, 400italic, bold, etc.
$hasTypes = false;
if (isset($fontValues['types']) && $fontValues['types']) {
$wfConfigGoogleFamily .= ':'.$fontValues['types'];
$hasTypes = true;
}
if ($subSetsStr) {
// If there are types, continue to use the comma delimiter
$wfConfigGoogleFamily .= ($hasTypes ? ',' : ':') . $subSetsStr;
}
// Append extra parameters to the last family from the list
if ($iCount === count($fontsArray['families']) - 1) {
// Effects
if (isset($fontsArray['effects']) && ! empty($fontsArray['effects'])) {
sort($fontsArray['effects']);
$wfConfigGoogleFamily .= '&effect=' . implode('|', array_unique($fontsArray['effects']));
}
if ($fontDisplay = Main::instance()->settings['google_fonts_display']) {
$wfConfigGoogleFamily .= '&display=' . $fontDisplay;
}
}
$wfConfigGoogleFamilies[] = "'".$wfConfigGoogleFamily."'";
$iCount++;
}
$wfConfigGoogleFamiliesStr = '['.implode(',', $wfConfigGoogleFamilies).']';
$finalInlineTagWebFontConfig = '<script id=\'wpacu-google-fonts-async-load\' type=\'text/javascript\'>'."\n".'WebFontConfig={google:{families:'.$wfConfigGoogleFamiliesStr.'}};(function(wpacuD){var wpacuWf=wpacuD.createElement(\'script\'),wpacuS=wpacuD.scripts[0];wpacuWf.src=(\'https:\'===document.location.protocol?\'https\':\'http\')+\'://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js\';wpacuWf.async=!0;wpacuS.parentNode.insertBefore(wpacuWf,wpacuS)})(document);'."\n".'</script>';
$htmlSource = str_replace(
array(
self::COMBINED_LINK_DEL,
self::NOSCRIPT_WEB_FONT_LOADER
),
array(
apply_filters( 'wpacu_combined_google_fonts_inline_script_tag', $finalInlineTagWebFontConfig ),
'<noscript>' . apply_filters( 'wpacu_combined_google_fonts_link_tag', $finalCombinedLink ) . '</noscript>' . "\n"
),
$htmlSource
);
}
}
return $htmlSource;
}
/**
* e.g. 300, 400, 400italic, bold, etc.
*
* @param $types
*
* @return string
*/
public static function buildSortTypesList($types)
{
$newTypes = array();
// More than one type per family?
if (strpos($types, ',') !== false) {
$multipleTypes = explode(',', trim($types, ','));
foreach ($multipleTypes as $type) {
if (trim($type)) {
$newTypes[] = trim($type);
}
}
} else {
// Only one type per family
$newTypes[] = $types;
}
$newTypes = array_unique($newTypes);
sort($newTypes);
return implode(',', $newTypes);
}
/**
* @return bool
*/
public static function preventAnyChange()
{
return defined( 'WPACU_ALLOW_ONLY_UNLOAD_RULES' ) && WPACU_ALLOW_ONLY_UNLOAD_RULES;
}
}

View File

@ -0,0 +1,234 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
/**
* Class FontsGoogle
* @package WpAssetCleanUp\OptimiseAssets
*/
class FontsGoogleRemove
{
/**
* @var array
*/
public static $stringsToCheck = array(
'//fonts.googleapis.com',
'//fonts.gstatic.com'
);
/**
* @var array
*/
public static $possibleWebFontConfigCdnPatterns = array(
'//ajax.googleapis.com/ajax/libs/webfont/(.*?)', // Google Apis
'//cdnjs.cloudflare.com/ajax/libs/webfont/(.*?)', // Cloudflare
'//cdn.jsdelivr.net/npm/webfontloader@(.*?)' // jsDELIVR
);
/**
* Called late from OptimizeCss after all other optimizations are done (e.g. minify, combine)
*
* @param $htmlSource
*
* @return mixed
*/
public static function cleanHtmlSource($htmlSource)
{
$htmlSource = self::cleanLinkTags($htmlSource);
$htmlSource = self::cleanFromInlineStyleTags($htmlSource);
return str_replace(FontsGoogle::NOSCRIPT_WEB_FONT_LOADER, '', $htmlSource);
}
/**
* @param $htmlSource
*
* @return mixed
*/
public static function cleanLinkTags($htmlSource)
{
// Do not continue if there is no single reference to the string we look for in the clean HTML source
if (stripos($htmlSource, FontsGoogle::$containsStr) === false) {
return $htmlSource;
}
// Get all valid LINKs that have the self::$stringsToCheck within them
$strContainsArray = array_map(static function($containsStr) {
return preg_quote($containsStr, '/');
}, self::$stringsToCheck);
$strContainsFormat = implode('|', $strContainsArray);
preg_match_all('#<link[^>]*(' . $strContainsFormat . ').*(>)#Usmi', $htmlSource, $matchesFromLinkTags, PREG_SET_ORDER);
$stripLinksList = array();
// Needs to match at least one to carry on with the replacements
if (isset($matchesFromLinkTags[0]) && ! empty($matchesFromLinkTags[0])) {
foreach ($matchesFromLinkTags as $linkTagArray) {
$linkTag = trim(trim($linkTagArray[0], '"\''));
if (strip_tags($linkTag) !== '') {
continue; // Something might be funny there, make sure the tag is valid
}
// Check if the Google Fonts CSS has any 'data-wpacu-skip' attribute; if it does, do not remove it
if (preg_match('#data-wpacu-skip([=>/ ])#i', $linkTag)) {
continue;
}
$stripLinksList[$linkTag] = '';
}
$htmlSource = strtr($htmlSource, $stripLinksList);
}
return $htmlSource;
}
/**
* @param $htmlSource
*
* @return mixed
*/
public static function cleanFromInlineStyleTags($htmlSource)
{
if (! preg_match('/(;?)(@import (?<url>url\(|\()?(?P<quotes>["\'()]?).+?(?P=quotes)(?(url)\)));?/', $htmlSource)) {
return $htmlSource;
}
preg_match_all('#<\s*?style\b[^>]*>(.*?)</style\b[^>]*>#s', $htmlSource, $styleMatches, PREG_SET_ORDER);
if (empty($styleMatches)) {
return $htmlSource;
}
// Go through each STYLE tag
foreach ($styleMatches as $styleInlineArray) {
list($styleInlineTag, $styleInlineContent) = $styleInlineArray;
// Check if the STYLE tag has any 'data-wpacu-skip' attribute; if it does, do not continue
if (preg_match('#data-wpacu-skip([=>/ ])#i', $styleInlineTag)) {
continue;
}
$newStyleInlineTag = $styleInlineTag;
$newStyleInlineContent = $styleInlineContent;
// Is the content relevant?
preg_match_all('/(;?)(@import (?<url>url\(|\()?(?P<quotes>["\'()]?).+?(?P=quotes)(?(url)\)));?/', $styleInlineContent, $matches);
if (isset($matches[0]) && ! empty($matches[0])) {
foreach ($matches[0] as $matchedImport) {
$newStyleInlineContent = str_replace($matchedImport, '', $newStyleInlineContent);
}
$newStyleInlineContent = trim($newStyleInlineContent);
// Is the STYLE tag empty after the @imports are removed? It happens on some websites; strip the tag, no point of having it empty
if ($newStyleInlineContent === '') {
$htmlSource = str_replace($styleInlineTag, '', $htmlSource);
} else {
$newStyleInlineTag = str_replace($styleInlineContent, $newStyleInlineContent, $styleInlineTag);
$htmlSource = str_replace($styleInlineTag, $newStyleInlineTag, $htmlSource);
}
}
$styleTagAfterImportsCleaned = $newStyleInlineTag;
$styleTagAfterFontFaceCleaned = trim(self::cleanFontFaceReferences($newStyleInlineContent));
$newStyleInlineTag = str_replace($newStyleInlineContent, $styleTagAfterFontFaceCleaned, $newStyleInlineTag);
$htmlSource = str_replace($styleTagAfterImportsCleaned, $newStyleInlineTag, $htmlSource);
}
return $htmlSource;
}
/**
* @param $importsAddToTop
*
* @return mixed
*/
public static function stripGoogleApisImport($importsAddToTop)
{
// Remove any Google Fonts imports
foreach ($importsAddToTop as $importKey => $importToPrepend) {
if (stripos($importToPrepend, FontsGoogle::$containsStr) !== false) {
unset($importsAddToTop[$importKey]);
}
}
return $importsAddToTop;
}
/**
* If "Google Font Remove" is active, strip its references from JavaScript code as well
*
* @param $jsContent
*
* @return string|string[]|null
*/
public static function stripReferencesFromJsCode($jsContent)
{
if (self::preventAnyChange()) {
return $jsContent;
}
$webFontConfigReferenceOne = "#src(\s+|)=(\s+|)(?<startDel>'|\")(\s+|)((http:|https:|)(".implode('|', self::$possibleWebFontConfigCdnPatterns).")(\s+|))(?<endDel>'|\")#si";
if (stripos($jsContent, 'WebFontConfig') !== false
&& preg_match('/(WebFontConfig\.|\'|"|)google(\s+|)([\'":=])/i', $jsContent)
&& preg_match_all($webFontConfigReferenceOne, $jsContent, $matches) && ! empty($matches)
) {
foreach ($matches[0] as $matchIndex => $matchRow) {
$jsContent = str_replace(
$matchRow,
'src=' . $matches['startDel'][$matchIndex] . $matches['endDel'][$matchIndex] . ';/* Stripped by ' . WPACU_PLUGIN_TITLE . ' */',
$jsContent
);
}
}
$webFontConfigReferenceTwo = '#("|\')((http:|https:|)//fonts.googleapis.com/(.*?))("|\')#si';
if (preg_match($webFontConfigReferenceTwo, $jsContent)) {
$jsContent = preg_replace($webFontConfigReferenceTwo, '\\1\\5', $jsContent);
}
return $jsContent;
}
/**
* @param $cssContent
*
* @return mixed
*/
public static function cleanFontFaceReferences($cssContent)
{
if (self::preventAnyChange()) {
return $cssContent;
}
preg_match_all('#@font-face(|\s+){(.*?)}#si', $cssContent, $matchesFromCssCode, PREG_SET_ORDER);
if (! empty($matchesFromCssCode)) {
foreach ($matchesFromCssCode as $matches) {
$fontFaceSyntax = $matches[0];
preg_match_all('/url(\s+|)\((?![\'"]?(?:data):)[\'"]?([^\'")]*)[\'"]?\)/i', $matches[0], $matchesFromUrlSyntax);
if (! empty($matchesFromUrlSyntax) && stripos(implode('', $matchesFromUrlSyntax[0]), '//fonts.gstatic.com/') !== false) {
$cssContent = str_replace($fontFaceSyntax, '', $cssContent);
}
}
}
return $cssContent;
}
/**
* @return bool
*/
public static function preventAnyChange()
{
return defined( 'WPACU_ALLOW_ONLY_UNLOAD_RULES' ) && WPACU_ALLOW_ONLY_UNLOAD_RULES;
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Plugin;
/**
* Class LocalFonts
* @package WpAssetCleanUp\OptimiseAssets
*/
class FontsLocal
{
/**
*
*/
public function init()
{
if (self::preventAnyChange()) {
return;
}
add_action('wp_head', array($this, 'preloadFontFiles'), 1);
}
/**
*
*/
public function preloadFontFiles()
{
// AMP page or Test Mode? Do not print anything
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive() ) {
return;
}
if (! $preloadFontFiles = trim(Main::instance()->settings['local_fonts_preload_files'])) {
return;
}
$preloadFontFilesArray = array();
if (strpos($preloadFontFiles, "\n") !== false) {
foreach (explode("\n", $preloadFontFiles) as $preloadFontFile) {
$preloadFontFile = trim($preloadFontFile);
if (! $preloadFontFile) {
continue;
}
$preloadFontFilesArray[] = $preloadFontFile;
}
} else {
$preloadFontFilesArray[] = $preloadFontFiles;
}
$preloadFontFilesArray = array_unique($preloadFontFilesArray);
$preloadFontFilesOutput = '';
// Finally, go through the list
foreach ($preloadFontFilesArray as $preloadFontFile) {
$preloadFontFilesOutput .= '<link rel="preload" as="font" href="'.esc_attr($preloadFontFile).'" data-wpacu-preload-font="1" crossorigin>'."\n";
}
echo apply_filters('wpacu_preload_local_font_files_output', $preloadFontFilesOutput);
}
/**
* @return bool
*/
public static function preventAnyChange()
{
if (defined('WPACU_ALLOW_ONLY_UNLOAD_RULES') && WPACU_ALLOW_ONLY_UNLOAD_RULES) {
return true;
}
return false;
}
}

View File

@ -0,0 +1,340 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Menu;
use WpAssetCleanUp\MetaBoxes;
use WpAssetCleanUp\Misc;
/**
* Class MinifyCss
* @package WpAssetCleanUp\OptimiseAssets
*/
class MinifyCss
{
/**
* @param $cssContent
* @param bool $forInlineStyle
*
* @return string
*/
public static function applyMinification($cssContent, $forInlineStyle = false
)
{
if (class_exists('\MatthiasMullie\Minify\CSS')) {
$sha1OriginalContent = sha1($cssContent);
$checkForAlreadyMinifiedShaOne = mb_strlen($cssContent) > 40000;
// Let's check if the content is already minified
// Save resources as the minify process can take time if the content is very large
// Limit the total number of entries tp 100: if it's more than that, it's likely because there's dynamic JS altering on every page load
if ($checkForAlreadyMinifiedShaOne && OptimizeCommon::originalContentIsAlreadyMarkedAsMinified($sha1OriginalContent, 'styles')) {
return $cssContent;
}
$cssContentBeforeAnyBugChanges = $cssContent;
// [CUSTOM BUG FIX]
// Encode the special matched content to avoid any wrong minification from the minifier
$hasVarWithZeroUnit = false;
preg_match_all('#--([a-zA-Z0-9_-]+):(\s+)0(em|ex|%|px|cm|mm|in|pt|pc|ch|rem|vh|vw|vmin|vmax|vm)#', $cssContent, $cssVariablesMatches);
if (isset($cssVariablesMatches[0]) && ! empty($cssVariablesMatches[0])) {
$hasVarWithZeroUnit = true;
foreach ($cssVariablesMatches[0] as $zeroUnitMatch) {
$cssContent = str_replace( $zeroUnitMatch, '[wpacu]' . base64_encode( $zeroUnitMatch ) . '[/wpacu]', $cssContent );
}
}
// Fix: If the content is something like "calc(50% - 22px) calc(50% - 22px);" then leave it as it is
preg_match_all('#calc(|\s+)\((.*?)(;|})#si', $cssContent, $cssCalcMatches);
$multipleOrSpecificCalcMatches = array(); // with multiple calc() or with at least one calc() that contains new lines
if (isset($cssCalcMatches[0]) && ! empty($cssCalcMatches[0])) {
foreach ($cssCalcMatches[0] as $cssCalcMatch) {
if (substr_count($cssCalcMatch, 'calc') > 1 || strpos($cssCalcMatch, "\n") !== false) {
$cssContent = str_replace( $cssCalcMatch, '[wpacu]' . base64_encode( $cssCalcMatch ) . '[/wpacu]', $cssContent );
$multipleOrSpecificCalcMatches[] = $cssCalcMatch;
}
}
}
// [/CUSTOM BUG FIX]
$minifier = new \MatthiasMullie\Minify\CSS( $cssContent );
if ( $forInlineStyle ) {
// If the minification is applied for inlined CSS (within STYLE) leave the background URLs unchanged as it sometimes lead to issues
$minifier->setImportExtensions( array() );
}
$minifiedContent = trim( $minifier->minify() );
// [CUSTOM BUG FIX]
// Restore the original content
if ($hasVarWithZeroUnit) {
foreach ( $cssVariablesMatches[0] as $zeroUnitMatch ) {
$zeroUnitMatchAlt = str_replace(': 0', ':0', $zeroUnitMatch); // remove the space
$minifiedContent = str_replace( '[wpacu]' . base64_encode( $zeroUnitMatch ) . '[/wpacu]', $zeroUnitMatchAlt, $minifiedContent );
}
}
if ( ! empty($multipleOrSpecificCalcMatches) ) {
foreach ( $multipleOrSpecificCalcMatches as $cssCalcMatch ) {
$originalCssCalcMatch = $cssCalcMatch;
$cssCalcMatch = preg_replace(array('#calc\(\s+#', '#\s+\);#'), array('calc(', ');'), $originalCssCalcMatch);
$cssCalcMatch = str_replace(' ) calc(', ') calc(', $cssCalcMatch);
$minifiedContent = str_replace( '[wpacu]' . base64_encode( $originalCssCalcMatch ) . '[/wpacu]', $cssCalcMatch, $minifiedContent );
}
}
// [/CUSTOM BUG FIX]
// Is there any [wpacu] left? Hmm, the replacement wasn't alright. Make sure to use the original minified version
if (strpos($minifiedContent, '[wpacu]') !== false && strpos($minifiedContent, '[/wpacu]') !== false) {
$minifier = new \MatthiasMullie\Minify\CSS( $cssContentBeforeAnyBugChanges );
if ( $forInlineStyle ) {
// If the minification is applied for inlined CSS (within STYLE) leave the background URLs unchanged as it sometimes leads to issues
$minifier->setImportExtensions( array() );
}
$minifiedContent = trim( $minifier->minify() );
}
if ($checkForAlreadyMinifiedShaOne && $minifiedContent === $cssContent) {
// If the resulting content is the same, mark it as minified to avoid the minify process next time
OptimizeCommon::originalContentMarkAsAlreadyMinified( $sha1OriginalContent, 'styles' );
}
return $minifiedContent;
}
return $cssContent;
}
/**
* @param $href
* @param string $handle
*
* @return bool
*/
public static function skipMinify($href, $handle = '')
{
// Things like WP Fastest Cache Toolbar CSS shouldn't be minified and take up space on the server
if ($handle !== '' && in_array($handle, Main::instance()->skipAssets['styles'])) {
return true;
}
// Some of these files (e.g. from Oxygen, WooCommerce) are already minified
$regExps = array(
'#/wp-content/plugins/wp-asset-clean-up(.*?).min.css#',
// Formidable Forms
'#/wp-content/plugins/formidable/css/formidableforms.css#',
// Oxygen
//'#/wp-content/plugins/oxygen/component-framework/oxygen.css#',
// WooCommerce
'#/wp-content/plugins/woocommerce/assets/css/woocommerce-layout.css#',
'#/wp-content/plugins/woocommerce/assets/css/woocommerce.css#',
'#/wp-content/plugins/woocommerce/assets/css/woocommerce-smallscreen.css#',
'#/wp-content/plugins/woocommerce/assets/css/blocks/style.css#',
'#/wp-content/plugins/woocommerce/packages/woocommerce-blocks/build/style.css#',
// Google Site Kit: the files are already optimized
'#/wp-content/plugins/google-site-kit/#',
// Other libraries from the core that end in .min.css
'#/wp-includes/css/(.*?).min.css#',
// Files within /wp-content/uploads/ or /wp-content/cache/
// Could belong to plugins such as "Elementor", "Oxygen" etc.
'#/wp-content/uploads/elementor/(.*?).css#',
'#/wp-content/uploads/oxygen/css/(.*?)-(.*?).css#',
'#/wp-content/cache/(.*?).css#',
// Already minified, and it also has a random name making the cache folder make bigger
'#/wp-content/bs-booster-cache/#',
);
$regExps = Misc::replaceRelPluginPath($regExps);
if (Main::instance()->settings['minify_loaded_css_exceptions'] !== '') {
$loadedCssExceptionsPatterns = trim(Main::instance()->settings['minify_loaded_css_exceptions']);
if (strpos($loadedCssExceptionsPatterns, "\n")) {
// Multiple values (one per line)
foreach (explode("\n", $loadedCssExceptionsPatterns) as $loadedCssExceptionPattern) {
$regExps[] = '#'.trim($loadedCssExceptionPattern).'#';
}
} else {
// Only one value?
$regExps[] = '#'.trim($loadedCssExceptionsPatterns).'#';
}
}
foreach ($regExps as $regExp) {
if ( preg_match( $regExp, $href ) || ( strpos($href, $regExp) !== false ) ) {
return true;
}
}
return false;
}
/**
* @param $htmlSource
*
* @return mixed|string
*/
public static function minifyInlineStyleTags($htmlSource)
{
if (stripos($htmlSource, '<style') === false) {
return $htmlSource; // no STYLE tags
}
$skipTagsContaining = array(
'data-wpacu-skip',
'astra-theme-css-inline-css',
'astra-edd-inline-css',
'et-builder-module-design-cached-inline-styles',
'fusion-stylesheet-inline-css',
'woocommerce-general-inline-css',
'woocommerce-inline-inline-css',
'data-wpacu-own-inline-style',
// Only shown to the admin, irrelevant for any optimization (save resources)
'data-wpacu-inline-css-file'
// already minified/optimized since the INLINE was generated from the cached file
);
$fetchType = 'regex';
if ( $fetchType === 'regex' ) {
preg_match_all( '@(<style[^>]*?>).*?</style>@si', $htmlSource, $matchesStyleTags, PREG_SET_ORDER );
if ( $matchesStyleTags === null ) {
return $htmlSource;
}
foreach ($matchesStyleTags as $matchedStyle) {
if ( ! (isset($matchedStyle[0]) && $matchedStyle[0]) ) {
continue;
}
$originalTag = $matchedStyle[0];
if (substr($originalTag, -strlen('></style>')) === strtolower('></style>')) {
// No empty STYLE tags
continue;
}
// No need to use extra resources as the tag is already minified
if ( preg_match( '(' . implode( '|', $skipTagsContaining ) . ')', $originalTag ) ) {
continue;
}
$tagOpen = $matchedStyle[1];
$withTagOpenStripped = substr($originalTag, strlen($tagOpen));
$originalTagContents = substr($withTagOpenStripped, 0, -strlen('</style>'));
if ( $originalTagContents ) {
$newTagContents = OptimizeCss::maybeAlterContentForInlineStyleTag( $originalTagContents, true, array( 'just_minify' ) );
// Only comments or no content added to the inline STYLE tag? Strip it completely to reduce the number of DOM elements
if ( $newTagContents === '/**/' || ! $newTagContents ) {
$htmlSource = str_replace( '>' . $originalTagContents . '</', '></', $htmlSource );
preg_match( '#<style.*?>#si', $originalTag, $matchFromStyle );
if ( isset( $matchFromStyle[0] ) && $styleTagWithoutContent = $matchFromStyle[0] ) {
$styleTagWithoutContentAlt = str_ireplace( '"', '\'', $styleTagWithoutContent );
$htmlSource = str_ireplace( array(
$styleTagWithoutContent . '</style>',
$styleTagWithoutContentAlt . '</style>'
), '', $htmlSource );
}
} else {
// It has content; do the replacement
$htmlSource = str_replace(
'>' . $originalTagContents . '</style>',
'>' . $newTagContents . '</style>',
$htmlSource
);
}
}
}
}
return $htmlSource;
}
/**
* @return bool
*/
public static function isMinifyCssEnabled()
{
if (defined('WPACU_IS_MINIFY_CSS_ENABLED')) {
return WPACU_IS_MINIFY_CSS_ENABLED;
}
// Request Minify On The Fly
// It will preview the page with CSS minified
// Only if the admin is logged-in as it uses more resources (CPU / Memory)
if ( isset($_GET['wpacu_css_minify']) && Menu::userCanManageAssets() ) {
self::isMinifyCssEnabledChecked('true');
return true;
}
if ( isset($_REQUEST['wpacu_no_css_minify']) || // not on query string request (debugging purposes)
is_admin() || // not for Dashboard view
(! Main::instance()->settings['minify_loaded_css']) || // Minify CSS has to be Enabled
(Main::instance()->settings['test_mode'] && ! Menu::userCanManageAssets()) ) { // Does not trigger if "Test Mode" is Enabled
self::isMinifyCssEnabledChecked('false');
return false;
}
$isSingularPage = defined('WPACU_CURRENT_PAGE_ID') && WPACU_CURRENT_PAGE_ID > 0 && is_singular();
if ($isSingularPage || Misc::isHomePage()) {
// If "Do not minify CSS on this page" is checked in "Asset CleanUp: Options" side meta box
if ($isSingularPage) {
$pageOptions = MetaBoxes::getPageOptions( WPACU_CURRENT_PAGE_ID ); // Singular page
} else {
$pageOptions = MetaBoxes::getPageOptions(0, 'front_page'); // Home page
}
if ( isset( $pageOptions['no_css_minify'] ) && $pageOptions['no_css_minify'] ) {
self::isMinifyCssEnabledChecked('false');
return false;
}
}
if (OptimizeCss::isOptimizeCssEnabledByOtherParty('if_enabled')) {
self::isMinifyCssEnabledChecked('false');
return false;
}
self::isMinifyCssEnabledChecked('true');
return true;
}
/**
* @param $value
*/
public static function isMinifyCssEnabledChecked($value)
{
if (! defined('WPACU_IS_MINIFY_CSS_ENABLED')) {
if ($value === 'true') {
define( 'WPACU_IS_MINIFY_CSS_ENABLED', true );
} elseif ($value === 'false') {
define( 'WPACU_IS_MINIFY_CSS_ENABLED', false );
}
}
}
}

View File

@ -0,0 +1,268 @@
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\Menu;
use WpAssetCleanUp\MetaBoxes;
use WpAssetCleanUp\Misc;
/**
* Class MinifyJs
* @package WpAssetCleanUp\OptimiseAssets
*/
class MinifyJs
{
/**
* @param $jsContent
*
* @return string|string[]|null
*/
public static function applyMinification($jsContent)
{
if (class_exists('\MatthiasMullie\Minify\JS')) {
$sha1OriginalContent = sha1($jsContent);
$checkForAlreadyMinifiedShaOne = mb_strlen($jsContent) > 40000;
// Let's check if the content is already minified
// Save resources as the minify process can take time if the content is very large
// Limit the total number of entries to 100: if it's more than that, it's likely because there's dynamic JS altering on every page load
if ($checkForAlreadyMinifiedShaOne && OptimizeCommon::originalContentIsAlreadyMarkedAsMinified($sha1OriginalContent, 'scripts')) {
return $jsContent;
}
// Minify it
$alreadyMinified = false; // default
$minifier = new \MatthiasMullie\Minify\JS($jsContent);
$minifiedContent = trim($minifier->minify());
if (trim($minifiedContent) === trim(trim($jsContent, ';'))) {
$minifiedContent = $jsContent; // consider them the same if only the ';' at the end was stripped (it doesn't worth the resources that would be used)
$alreadyMinified = true;
}
// If the resulting content is the same, mark it as minified to avoid the minify process next time
if ($checkForAlreadyMinifiedShaOne && $alreadyMinified) {
// If the resulting content is the same, mark it as minified to avoid the minify process next time
OptimizeCommon::originalContentMarkAsAlreadyMinified( $sha1OriginalContent, 'scripts' );
}
return $minifiedContent;
}
return $jsContent;
}
/**
* @param $src
* @param string $handle
*
* @return bool
*/
public static function skipMinify($src, $handle = '')
{
// Things like WP Fastest Cache Toolbar JS shouldn't be minified and take up space on the server
if ($handle !== '' && in_array($handle, Main::instance()->skipAssets['scripts'])) {
return true;
}
$regExps = array(
'#/wp-content/plugins/wp-asset-clean-up(.*?).js#',
// Other libraries from the core that end in .min.js
'#/wp-includes/(.*?).min.js#',
// jQuery & jQuery Migrate
'#/wp-includes/js/jquery/jquery.js#',
'#/wp-includes/js/jquery/jquery-migrate.js#',
// Files within /wp-content/uploads/
// Files within /wp-content/uploads/ or /wp-content/cache/
// Could belong to plugins such as "Elementor, "Oxygen" etc.
//'#/wp-content/uploads/(.*?).js#',
'#/wp-content/cache/(.*?).js#',
// Already minified, and it also has a random name making the cache folder make bigger
'#/wp-content/bs-booster-cache/#',
// Elementor .min.js
'#/wp-content/plugins/elementor/assets/(.*?).min.js#',
// WooCommerce Assets
'#/wp-content/plugins/woocommerce/assets/js/(.*?).min.js#',
// Google Site Kit
// The files are already optimized (they just have comments once in a while)
// Minifying them causes some errors, so better to leave them load as they are
'#/wp-content/plugins/google-site-kit/#',
// TranslatePress Multilingual
'#/translatepress-multilingual/assets/js/trp-editor.js#',
);
$regExps = Misc::replaceRelPluginPath($regExps);
if (Main::instance()->settings['minify_loaded_js_exceptions'] !== '') {
$loadedJsExceptionsPatterns = trim(Main::instance()->settings['minify_loaded_js_exceptions']);
if (strpos($loadedJsExceptionsPatterns, "\n")) {
// Multiple values (one per line)
foreach (explode("\n", $loadedJsExceptionsPatterns) as $loadedJsExceptionPattern) {
$regExps[] = '#'.trim($loadedJsExceptionPattern).'#';
}
} else {
// Only one value?
$regExps[] = '#'.trim($loadedJsExceptionsPatterns).'#';
}
}
foreach ($regExps as $regExp) {
if ( preg_match( $regExp, $src ) || ( strpos($src, $regExp) !== false ) ) {
return true;
}
}
return false;
}
/**
* @param $htmlSource
*
* @return mixed|string
*/
public static function minifyInlineScriptTags($htmlSource)
{
if (stripos($htmlSource, '<script') === false) {
return $htmlSource; // no SCRIPT tags, hmm
}
$skipTagsContaining = array_map( static function ( $toMatch ) {
return preg_quote($toMatch, '/');
}, array(
'data-wpacu-skip',
'/* <![CDATA[ */', // added via wp_localize_script()
'wpacu-google-fonts-async-load',
'wpacu-preload-async-css-fallback',
/* [wpacu_pro] */'data-wpacu-inline-js-file',/* [/wpacu_pro] */
'document.body.prepend(wpacuLinkTag',
'var wc_product_block_data = JSON.parse( decodeURIComponent(',
'/(^|\s)(no-)?customize-support(?=\s|$)/', // WP Core
'b[c] += ( window.postMessage && request ? \' \' : \' no-\' ) + cs;', // WP Core
'data-wpacu-own-inline-script', // Only shown to the admin, irrelevant for any optimization (save resources)
// [wpacu_pro]
'data-wpacu-inline-js-file', // already minified/optimized since the INLINE was generated from the cached file
// [/wpacu_pro]
));
// Do not perform another \DOMDocument call if it was done already somewhere else (e.g. CombineJs)
$fetchType = 'regex'; // 'regex' or 'dom'
if ($fetchType === 'regex') {
preg_match_all( '@(<script[^>]*?>).*?</script>@si', $htmlSource, $matchesScriptTags, PREG_SET_ORDER );
if ( $matchesScriptTags === null ) {
return $htmlSource;
}
foreach ($matchesScriptTags as $matchedScript) {
if (isset($matchedScript[0]) && $matchedScript[0]) {
$originalTag = $matchedScript[0];
if (strpos($originalTag, 'src=') && strtolower(substr($originalTag, -strlen('></script>'))) === strtolower('></script>')) {
// Only inline SCRIPT tags allowed
continue;
}
// No need to use extra resources as the tag is already minified
if ( preg_match( '/(' . implode( '|', $skipTagsContaining ) . ')/', $originalTag ) ) {
continue;
}
// Only 'text/javascript' type is allowed for minification
$scriptType = Misc::getValueFromTag($originalTag, 'type') ?: 'text/javascript'; // default
if ($scriptType !== 'text/javascript') {
continue;
}
$tagOpen = $matchedScript[1];
$withTagOpenStripped = substr($originalTag, strlen($tagOpen));
$originalTagContents = substr($withTagOpenStripped, 0, -strlen('</script>'));
$newTagContents = OptimizeJs::maybeAlterContentForInlineScriptTag( $originalTagContents, true );
if ( $newTagContents !== $originalTagContents ) {
$htmlSource = str_ireplace( '>' . $originalTagContents . '</script', '>' . $newTagContents . '</script', $htmlSource );
}
}
}
}
return $htmlSource;
}
/**
* @return bool
*/
public static function isMinifyJsEnabled()
{
if (defined('WPACU_IS_MINIFY_JS_ENABLED')) {
return WPACU_IS_MINIFY_JS_ENABLED;
}
// Request Minify On The Fly
// It will preview the page with JS minified
// Only if the admin is logged-in as it uses more resources (CPU / Memory)
if ( isset($_GET['wpacu_js_minify']) && Menu::userCanManageAssets()) {
self::isMinifyJsEnabledChecked('true');
return true;
}
if ( isset($_REQUEST['wpacu_no_js_minify']) || // not on query string request (debugging purposes)
is_admin() || // not for Dashboard view
(! Main::instance()->settings['minify_loaded_js']) || // Minify JS has to be Enabled
(Main::instance()->settings['test_mode'] && ! Menu::userCanManageAssets()) ) { // Does not trigger if "Test Mode" is Enabled
self::isMinifyJsEnabledChecked('false');
return false;
}
$isSingularPage = defined('WPACU_CURRENT_PAGE_ID') && WPACU_CURRENT_PAGE_ID > 0 && is_singular();
if ($isSingularPage || Misc::isHomePage()) {
// If "Do not minify JS on this page" is checked in "Asset CleanUp: Options" side meta box
if ($isSingularPage) {
$pageOptions = MetaBoxes::getPageOptions( WPACU_CURRENT_PAGE_ID ); // Singular page
} else {
$pageOptions = MetaBoxes::getPageOptions(0, 'front_page'); // Home page
}
if ( isset( $pageOptions['no_js_minify'] ) && $pageOptions['no_js_minify'] ) {
self::isMinifyJsEnabledChecked('false');
return false;
}
}
if (OptimizeJs::isOptimizeJsEnabledByOtherParty('if_enabled')) {
self::isMinifyJsEnabledChecked('false');
return false;
}
self::isMinifyJsEnabledChecked('true');
return true;
}
/**
* @param $value
*/
public static function isMinifyJsEnabledChecked($value)
{
if (! defined('WPACU_IS_MINIFY_JS_ENABLED')) {
if ($value === 'true') {
define( 'WPACU_IS_MINIFY_JS_ENABLED', true );
} elseif ($value === 'false') {
define( 'WPACU_IS_MINIFY_JS_ENABLED', false );
}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff