wp_back/wp-content/plugins/wp-asset-clean-up/classes/OptimiseAssets/OptimizeCss.php

1537 lines
55 KiB
PHP
Raw Normal View History

2024-05-20 15:37:46 +03:00
<?php
namespace WpAssetCleanUp\OptimiseAssets;
use WpAssetCleanUp\ObjectCache;
use WpAssetCleanUp\Plugin;
use WpAssetCleanUp\Preloads;
use WpAssetCleanUp\FileSystem;
use WpAssetCleanUp\CleanUp;
use WpAssetCleanUp\Main;
use WpAssetCleanUp\MetaBoxes;
use WpAssetCleanUp\Misc;
/**
* Class OptimizeCss
* @package WpAssetCleanUp
*/
class OptimizeCss
{
/**
*
*/
const MOVE_NOSCRIPT_TO_BODY_FOR_CERTAIN_LINK_TAGS = '<span style="display: none;" data-name=wpacu-delimiter data-content="ASSET CLEANUP NOSCRIPT FOR ASYNC PRELOADS"></span>';
/**
*
*/
public function init()
{
add_action('init', array($this, 'triggersAfterInit'));
add_action('wp_footer', static function() {
if ( Plugin::preventAnyFrontendOptimization() || Main::isTestModeActive() ) { return; }
/* [wpacu_timing] */ Misc::scriptExecTimer( 'prepare_optimize_files_css' ); /* [/wpacu_timing] */
self::prepareOptimizeList();
/* [wpacu_timing] */ Misc::scriptExecTimer( 'prepare_optimize_files_css', 'end' ); /* [/wpacu_timing] */
echo self::MOVE_NOSCRIPT_TO_BODY_FOR_CERTAIN_LINK_TAGS;
}, PHP_INT_MAX);
add_filter('wpacu_html_source_after_optimization', static function($htmlSource) {
// Are any the marks still there & weren't replaced? Strip them to have a clean HTML output!
return str_replace(self::MOVE_NOSCRIPT_TO_BODY_FOR_CERTAIN_LINK_TAGS, '', $htmlSource);
});
add_filter('wpacu_add_noscript_certain_link_tags', array($this, 'appendNoScriptCertainLinkTags'));
}
/**
*
*/
public function triggersAfterInit()
{
if (self::isInlineCssEnabled()) {
$allPatterns = self::getAllInlineChosenPatterns();
if (! empty($allPatterns)) {
// Make "Inline CSS Files" compatible with "Optimize CSS Delivery" from WP Rocket
add_filter('rocket_async_css_regex_pattern', static function($regex) {
return '/(?=<link(?!.*wpacu-to-be-inlined.*)[^>]*\s(rel\s*=\s*[\'"]stylesheet["\']))<link(?!.*wpacu-to-be-inlined.*)[^>]*\shref\s*=\s*[\'"]([^\'"]+)[\'"](.*)>/iU';
});
add_filter('style_loader_tag', static function($styleTag) use ($allPatterns) {
foreach ($allPatterns as $patternToCheck) {
preg_match_all( '#<link[^>]*stylesheet[^>]*('.$patternToCheck.').*(>)#Usmi', $styleTag, $matchesSourcesFromTags, PREG_SET_ORDER );
if ( ! empty( $matchesSourcesFromTags ) ) {
return str_replace( '<link ', '<link wpacu-to-be-inlined=\'1\' ', $styleTag );
}
}
return $styleTag;
}, 10, 1);
}
}
}
/**
* @return array
*/
public static function getAllInlineChosenPatterns()
{
$inlineCssFilesPatterns = trim(Main::instance()->settings['inline_css_files_list']);
$allPatterns = array();
if (strpos($inlineCssFilesPatterns, "\n")) {
// Multiple values (one per line)
foreach (explode("\n", $inlineCssFilesPatterns) as $inlinePattern) {
$allPatterns[] = trim($inlinePattern);
}
} else {
// Only one value?
$allPatterns[] = trim($inlineCssFilesPatterns);
}
// Strip any empty values
return array_filter($allPatterns);
}
/**
*
*/
public static function prepareOptimizeList()
{
if ( ! self::isWorthCheckingForOptimization() || Plugin::preventAnyFrontendOptimization() ) {
return;
}
global $wp_styles;
$allStylesHandles = ObjectCache::wpacu_cache_get('wpacu_all_styles_handles');
if (empty($allStylesHandles)) {
return;
}
// [Start] Collect for caching
$wpStylesDone = isset($wp_styles->done) && is_array($wp_styles->done) ? $wp_styles->done : array();
$wpStylesRegistered = isset($wp_styles->registered) && is_array($wp_styles->registered) ? $wp_styles->registered : array();
// Collect all enqueued clean (no query strings) HREFs to later compare them against any hardcoded CSS
$allEnqueuedCleanLinkHrefs = array();
if (! empty($wpStylesDone) && ! empty($wpStylesRegistered)) {
foreach ( $wpStylesDone as $index => $styleHandle ) {
if ( isset( Main::instance()->wpAllStyles['registered'][ $styleHandle ]->src ) && ( $src = Main::instance()->wpAllStyles['registered'][ $styleHandle ]->src ) ) {
$localAssetPath = OptimizeCommon::getLocalAssetPath( $src, 'css' );
if ( ! $localAssetPath || ! is_file( $localAssetPath ) ) {
continue; // not a local file
}
ob_start();
$wp_styles->do_item( $styleHandle );
$linkSourceTag = trim( ob_get_clean() );
// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not alter it
if ( preg_match( '#data-wpacu-skip([=>/ ])#i', $linkSourceTag ) ) {
unset( $wpStylesDone[ $index ] );
continue;
}
$cleanLinkHrefFromTagArray = OptimizeCommon::getLocalCleanSourceFromTag( $linkSourceTag );
if ( isset( $cleanLinkHrefFromTagArray['source'] ) && $cleanLinkHrefFromTagArray['source'] ) {
$allEnqueuedCleanLinkHrefs[] = $cleanLinkHrefFromTagArray['source'];
}
}
}
}
$cssOptimizeList = array();
if (! empty($wpStylesDone) && ! empty($wpStylesRegistered)) {
$isMinifyCssFilesEnabled = MinifyCss::isMinifyCssEnabled() && in_array(Main::instance()->settings['minify_loaded_css_for'], array('href', 'all', ''));
foreach ( $wpStylesDone as $handle ) {
if ( ! isset( $wpStylesRegistered[ $handle ]->src ) ) {
continue;
}
$value = $wpStylesRegistered[ $handle ];
$localAssetPath = OptimizeCommon::getLocalAssetPath( $value->src, 'css' );
if ( ! $localAssetPath || ! is_file( $localAssetPath ) ) {
continue; // not a local file
}
$optimizeValues = self::maybeOptimizeIt(
$value,
array( 'local_asset_path' => $localAssetPath, 'is_minify_css_enabled' => $isMinifyCssFilesEnabled )
);
ObjectCache::wpacu_cache_set( 'wpacu_maybe_optimize_it_css_' . $handle, $optimizeValues );
if ( ! empty( $optimizeValues ) ) {
$cssOptimizeList[] = $optimizeValues;
}
}
}
if (empty($cssOptimizeList)) {
return;
}
ObjectCache::wpacu_cache_add('wpacu_css_enqueued_hrefs', $allEnqueuedCleanLinkHrefs);
ObjectCache::wpacu_cache_add('wpacu_css_optimize_list', $cssOptimizeList);
// [End] Collect for caching
}
/**
* @param $value
* @param array $fileAlreadyChecked
*
* @return mixed
*/
public static function maybeOptimizeIt($value, $fileAlreadyChecked = array())
{
if ($optimizeValues = ObjectCache::wpacu_cache_get('wpacu_maybe_optimize_it_css_'.$value->handle)) {
return $optimizeValues;
}
global $wp_version;
$src = isset($value->src) ? $value->src : false;
if (! $src) {
return array();
}
$doFileMinify = true;
$isMinifyCssFilesEnabled = (isset($fileAlreadyChecked['is_minify_css_enabled']) && $fileAlreadyChecked['is_minify_css_enabled'])
? $fileAlreadyChecked['is_minify_css_enabled']
: MinifyCss::isMinifyCssEnabled() && in_array(Main::instance()->settings['minify_loaded_css_for'], array('href', 'all', ''));
if ( ! $isMinifyCssFilesEnabled || MinifyCss::skipMinify($src, $value->handle) ) {
$doFileMinify = false;
}
// Default (it will be later replaced with the last time the file was modified, which is more accurate)
$dbVer = (isset($value->ver) && $value->ver) ? $value->ver : $wp_version;
$isCssFile = false;
// Already checked? Do not reuse OptimizeCommon::getLocalAssetPath() and is_file()
if (isset($fileAlreadyChecked['local_asset_path']) && $fileAlreadyChecked['local_asset_path']) {
$localAssetPath = $fileAlreadyChecked['local_asset_path'];
$checkCond = $localAssetPath;
} else {
$localAssetPath = OptimizeCommon::getLocalAssetPath( $src, 'css' );
$checkCond = $localAssetPath && is_file($localAssetPath);
}
if ($checkCond) {
if ($fileMTime = @filemtime($localAssetPath)) {
$dbVer = $fileMTime;
}
$isCssFile = true;
}
if ($isCssFile) {
// This is the safest one as handle names for specific static can change on very page load
// as some developers have a habit of adding the UNIX time or other random string to a handle (e.g. for debugging)
$uniqueAssetStr = md5 ( str_replace(Misc::getWpRootDirPath(), '', $localAssetPath) );
} else {
$uniqueAssetStr = md5( $value->handle );
}
$transientName = 'wpacu_css_optimize_'.$uniqueAssetStr;
$skipCache = false;
if (isset($_GET['wpacu_no_cache']) || (defined('WPACU_NO_CACHE') && WPACU_NO_CACHE === true)) {
$skipCache = true;
}
if (! $skipCache) {
$savedValues = OptimizeCommon::getTransient($transientName);
if ( $savedValues ) {
$savedValuesArray = json_decode( $savedValues, ARRAY_A );
if ( $savedValuesArray['ver'] !== $dbVer ) {
// New File Version? Delete transient as it will be re-created with the new version
OptimizeCommon::deleteTransient($transientName);
} else {
$localPathToCssOptimized = str_replace( '//', '/', Misc::getWpRootDirPath() . $savedValuesArray['optimize_uri'] );
// Read the file from its caching (that makes the processing faster)
if ( isset( $savedValuesArray['source_uri'] ) && is_file( $localPathToCssOptimized ) ) {
if (Main::instance()->settings['fetch_cached_files_details_from'] === 'db_disk') {
$GLOBALS['wpacu_from_location_inc']++;
}
return array(
$savedValuesArray['source_uri'],
$savedValuesArray['optimize_uri'],
$value->src,
$value->handle
);
}
// If nothing valid gets returned above, make sure the transient gets deleted as it's re-added later on
OptimizeCommon::deleteTransient($transientName);
}
}
}
// Check if it starts without "/" or a protocol; e.g. "wp-content/theme/style.css"
if (strpos($src, '/') !== 0 &&
strpos($src, '//') !== 0 &&
stripos($src, 'http://') !== 0 &&
stripos($src, 'https://') !== 0
) {
$src = '/'.$src; // append the forward slash to be processed as relative later on
}
// Starts with '/', but not with '//'
if (strpos($src, '/') === 0 && strpos($src, '//') !== 0) {
$src = site_url() . $src;
}
if (Main::instance()->settings['cache_dynamic_loaded_css'] &&
$value->handle === 'sccss_style' &&
in_array('simple-custom-css/simple-custom-css.php', Misc::getActivePlugins())
) {
$pathToAssetDir = '';
$sourceBeforeOptimization = $value->src;
if (! ($cssContent = DynamicLoadedAssets::getAssetContentFrom('simple-custom-css', $value))) {
return array();
}
} elseif (Main::instance()->settings['cache_dynamic_loaded_css'] &&
((strpos($src, '/?') !== false) || (strpos($src, rtrim(site_url(),'/').'?') !== false) || (strpos($src, '.php?') !== false) || Misc::endsWith($src, '.php')) &&
(strpos($src, rtrim(site_url(), '/')) !== false)
) {
$pathToAssetDir = '';
$sourceBeforeOptimization = str_replace('&#038;', '&', $value->src);
if (! ($cssContent = DynamicLoadedAssets::getAssetContentFrom('dynamic', $value))) {
return array();
}
} else {
if (! $isCssFile) {
return array();
}
/*
* This is a local .CSS file
*/
$pathToAssetDir = OptimizeCommon::getPathToAssetDir($src);
$cssContent = FileSystem::fileGetContents($localAssetPath, 'combine_css_imports');
$sourceBeforeOptimization = str_replace(Misc::getWpRootDirPath(), '/', $localAssetPath);
}
$cssContent = trim($cssContent);
/*
* [START] CSS Content Optimization
*/
// If there are no changes from this point, do not optimize (keep the file where it is)
$cssContentBefore = $cssContent;
if ($cssContent) { // only proceed with extra alterations if there is some content there (save resources)
if ( Main::instance()->settings['google_fonts_display'] ) {
// Any "font-display" enabled in "Settings" - "Google Fonts"?
$cssContent = FontsGoogle::alterGoogleFontUrlFromCssContent( $cssContent );
}
// Move any @imports to top; This also strips any @imports to Google Fonts if the option is chosen
$cssContent = self::importsUpdate( $cssContent );
}
// If it stays like this, it means there is content there, even if only comments
$cssContentBecomesEmptyAfterMin = false;
if ($doFileMinify && $cssContent) { // only bother to minify it if it has any content, save resources
// Minify this file?
$cssContentBeforeMin = trim($cssContent);
$cssContentAfterMin = MinifyCss::applyMinification($cssContent);
$cssContent = $cssContentAfterMin;
if ($cssContentBeforeMin && $cssContentAfterMin === '') {
// It had content, but became empty after minification, most likely it had only comments (e.g. a default child theme's style)
$cssContentBecomesEmptyAfterMin = true;
}
}
if ($cssContentBecomesEmptyAfterMin || $cssContent === '') {
$cssContent = '/**/';
} else {
if ( Main::instance()->settings['google_fonts_remove'] ) {
$cssContent = FontsGoogleRemove::cleanFontFaceReferences( $cssContent );
}
// No changes were made, thus, there's no point in changing the original file location
if ( $isCssFile && ! $cssContentBecomesEmptyAfterMin && trim( $cssContentBefore ) === trim( $cssContent ) ) {
// There's no point in changing the original CSS (static) file location
return false;
}
// Continue, as changes are to be made
// Does it have a source map? Strip it
if (strpos($cssContent, '/*# sourceMappingURL=') !== false) {
$cssContent = OptimizeCommon::stripSourceMap($cssContent, 'css');
}
$cssContent = self::maybeFixCssContent( $cssContent, $pathToAssetDir . '/' ); // Path
}
/*
* [END] CSS Content Optimization
*/
// Relative path to the new file
// Save it to /wp-content/cache/css/{OptimizeCommon::$optimizedSingleFilesDir}/
/*
if ($fileVer !== $wp_version) {
if (is_array($fileVer)) {
// Convert to string if it's an array (rare cases)
$fileVer = implode('-', $fileVer);
}
$fileVer = trim(str_replace(' ', '_', preg_replace('/\s+/', ' ', $fileVer)));
$fileVer = (strlen($fileVer) > 50) ? substr(md5($fileVer), 0, 20) : $fileVer; // don't end up with too long filenames
}
*/
$fileVer = sha1($cssContent);
$uniqueCachedAssetName = OptimizeCommon::generateUniqueNameForCachedAsset($isCssFile, $localAssetPath, $value->handle, $fileVer);
$newFilePathUri = self::getRelPathCssCacheDir() . OptimizeCommon::$optimizedSingleFilesDir . '/' . $uniqueCachedAssetName;
$newFilePathUri .= '.css';
if ($cssContent === '') {
$cssContent = '/**/';
}
if ($cssContent === '/**/') {
// Leave a signature that the file is empty, thus it would be faster to take further actions upon it later on, saving resources)
$newFilePathUri = str_replace('.css', '-wpacu-empty-file.css', $newFilePathUri);
}
$newLocalPath = WP_CONTENT_DIR . $newFilePathUri; // Ful Local path
$newLocalPathUrl = WP_CONTENT_URL . $newFilePathUri; // Full URL path
if ($cssContent && $cssContent !== '/**/' && apply_filters('wpacu_print_info_comments_in_cached_assets', true)) {
$cssContent = '/*!' . $sourceBeforeOptimization . '*/' . $cssContent;
}
$saveFile = FileSystem::filePutContents($newLocalPath, $cssContent);
if (! $saveFile && ! $cssContent) {
// Fallback to the original CSS if the optimized version can't be created or updated
return array();
}
$saveValues = array(
'source_uri' => OptimizeCommon::getSourceRelPath($src),
'optimize_uri' => OptimizeCommon::getSourceRelPath($newLocalPathUrl),
'ver' => $dbVer
);
// Re-add transient
OptimizeCommon::setTransient($transientName, wp_json_encode($saveValues));
return array(
OptimizeCommon::getSourceRelPath($src), // Original SRC (Relative path)
OptimizeCommon::getSourceRelPath($newLocalPathUrl), // New SRC (Relative path)
$value->src, // SRC (as it is)
$value->handle
);
}
/**
* @param $htmlSource
*
* @return mixed|void
*/
public static function alterHtmlSource($htmlSource)
{
// There has to be at least one "<link" or "<style", otherwise, it could be a feed request or something similar (not page, post, homepage etc.)
if ( (stripos($htmlSource, '<link') === false && stripos($htmlSource, '<style') === false) || isset($_GET['wpacu_no_optimize_css']) ) {
return $htmlSource;
}
/* [wpacu_timing] */ Misc::scriptExecTimer('alter_html_source_for_optimize_css'); /* [/wpacu_timing] */
// Are there any assets unloaded where their "children" are ignored?
// Since they weren't dequeued the WP way (to avoid unloading the "children"), they will be stripped here
if (! Main::instance()->preventAssetsSettings()) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_unload_ignore_deps_css'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = self::ignoreDependencyRuleAndKeepChildrenLoaded($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
$htmlSource = self::stripAnyReferencesForUnloadedStyles($htmlSource);
if (self::isWorthCheckingForOptimization()) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_original_to_optimized_css'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
// 'wpacu_css_optimize_list' caching list is also checked; if it's empty, no optimization is made
$htmlSource = self::updateHtmlSourceOriginalToOptimizedCss($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
if (! Main::instance()->preventAssetsSettings()) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_preload_css'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = Preloads::instance()->doChanges($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
if (self::isInlineCssEnabled()) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_inline_css'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = self::doInline($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
$proceedWithCombineOnThisPage = true;
$isSingularPage = defined('WPACU_CURRENT_PAGE_ID') && WPACU_CURRENT_PAGE_ID > 0 && is_singular();
// If "Do not combine CSS on this page" is checked in "Asset CleanUp: Options" side meta box
// Works for posts, pages and custom post types
if ($isSingularPage || Misc::isHomePage()) {
if ($isSingularPage) {
$pageOptions = MetaBoxes::getPageOptions( WPACU_CURRENT_PAGE_ID ); // Singular page
} else {
$pageOptions = MetaBoxes::getPageOptions(0, 'front_page'); // Home page
}
// 'no_css_optimize' refers to avoid the combination of CSS files
if ( (isset( $pageOptions['no_css_optimize'] ) && $pageOptions['no_css_optimize'])
|| (isset( $pageOptions['no_assets_settings'] ) && $pageOptions['no_assets_settings']) ) {
$proceedWithCombineOnThisPage = false;
}
}
if ($proceedWithCombineOnThisPage) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_combine_css'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = CombineCss::doCombine($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
if (self::isWorthCheckingForOptimization() && ! Main::instance()->preventAssetsSettings() && (MinifyCss::isMinifyCssEnabled() && in_array(Main::instance()->settings['minify_loaded_css_for'], array('inline', 'all')))) {
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_minify_inline_style_tags'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = MinifyCss::minifyInlineStyleTags($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
}
// Final cleanups
$htmlSource = preg_replace('#<link(\s+|)data-wpacu-link-rel-href-before=(["\'])' . '(.*)' . '(\1)#Usmi', '<link ', $htmlSource);
//$htmlSource = preg_replace('#<link(.*)data-wpacu-style-handle=\'(.*)\'#Umi', '<link \\1', $htmlSource);
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_google_fonts_optimization_removal'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
// Alter HTML Source for Google Fonts Optimization / Removal
$htmlSource = FontsGoogle::alterHtmlSource($htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
// NOSCRIPT fallback: Applies for Google Fonts (async) (Lite and Pro) / Preloads (Async in Pro version) / Critical CSS (as LINK "stylesheet" tags will be async preloaded)
/* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_add_async_preloads_noscript'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */
$htmlSource = apply_filters('wpacu_add_noscript_certain_link_tags', $htmlSource);
/* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */
/* [wpacu_timing] */ Misc::scriptExecTimer('alter_html_source_for_optimize_css', 'end'); /* [/wpacu_timing] */
return $htmlSource;
}
/**
* @return string
*/
public static function getRelPathCssCacheDir()
{
return OptimizeCommon::getRelPathPluginCacheDir().'css/'; // keep trailing slash at the end
}
/**
* @param $firstLinkHref
* @param $htmlSource
*
* @return string
*/
public static function getFirstLinkTag($firstLinkHref, $htmlSource)
{
preg_match_all('#<link[^>]*stylesheet[^>]*(>)#Umi', $htmlSource, $matches);
foreach ($matches[0] as $matchTag) {
if (strpos($matchTag, $firstLinkHref) !== false) {
return trim($matchTag);
}
}
return '';
}
/**
*
* @param $cssContent
* @param $appendBefore
* @param $fix
*
* @return mixed
*/
public static function maybeFixCssContent($cssContent, $appendBefore, $fix = 'path')
{
// Updates (background | font etc.) URLs to the right path and others
if ($fix === 'path') {
// Clear any extra spaces between @import and the single/double quotes
$cssContent = preg_replace('/@import(\s+|)([\'"])/i', '@import \\2', $cssContent);
$cssContentPathReps = array(
// @import with url(), background-image etc.
'url("../' => 'url("'.$appendBefore.'../',
"url('../" => "url('".$appendBefore.'../',
'url(../' => 'url('.$appendBefore.'../',
'url("./' => 'url("'.$appendBefore.'./',
"url('./" => "url('".$appendBefore.'./',
'url(./' => 'url('.$appendBefore.'./',
// @import without URL
'@import "../' => '@import "'.$appendBefore.'../',
"@import '../" => "@import '".$appendBefore.'../',
'@import "./' => '@import "'.$appendBefore.'./',
"@import './" => "@import '".$appendBefore.'./'
);
$cssContent = str_replace(array_keys($cssContentPathReps), array_values($cssContentPathReps), $cssContent);
// Rare cases
$cssContent = preg_replace('/url\((\s+)http/i', 'url(http', $cssContent);
// Avoid Background URLs starting with "#", "data", "http" or "https" as they do not need to have a path updated
preg_match_all('/url\((?![\'"]?(?:#|data|http|https):)[\'"]?([^\'")]*)[\'"]?\)/i', $cssContent, $matches);
// If it starts with forward slash (/), it doesn't need fix, just skip it
// Also skip ../ types as they were already processed
$toSkipList = array("url('/", 'url("/', 'url(/');
foreach ($matches[0] as $match) {
$fullUrlMatch = trim($match);
foreach ($toSkipList as $toSkip) {
if (substr($fullUrlMatch, 0, strlen($toSkip)) === $toSkip) {
continue 2; // doesn't need any fix, go to the next match
}
}
// Go through all situations: with and without quotes, with traversal directory (e.g. ../../)
$alteredMatch = str_replace(
array('url("', "url('"),
array('url("' . $appendBefore, "url('" . $appendBefore),
$fullUrlMatch
);
$alteredMatch = trim($alteredMatch);
if (! in_array($fullUrlMatch[4], array("'", '"', '/', '.', '#'))) {
$alteredMatch = str_replace('url(', 'url(' . $appendBefore, $alteredMatch);
$alteredMatch = str_replace(array('")', '\')'), ')', $alteredMatch);
}
// Finally, apply the changes
$cssContent = str_replace($fullUrlMatch, $alteredMatch, $cssContent);
// Bug fix
$cssContent = str_replace(
array($appendBefore . '"' . $appendBefore, $appendBefore . "'" . $appendBefore),
$appendBefore,
$cssContent
);
// Bug Fix 2
$cssContent = str_replace($appendBefore . 'http', 'http', $cssContent);
$cssContent = str_replace($appendBefore . '//', '//', $cssContent);
}
}
return $cssContent;
}
/**
* Next: Alter the HTML source by updating the original link URLs with the just cached ones
*
* @param $htmlSource
*
* @return mixed
*/
public static function updateHtmlSourceOriginalToOptimizedCss($htmlSource)
{
$parseSiteUrlPath = (string)parse_url(site_url(), PHP_URL_PATH);
$siteUrlNoProtocol = str_replace(array('http://', 'https://'), '//', site_url());
$cssOptimizeList = ObjectCache::wpacu_cache_get('wpacu_css_optimize_list') ?: array();
$allEnqueuedCleanSources = ObjectCache::wpacu_cache_get('wpacu_css_enqueued_hrefs') ?: array();
$allEnqueuedCleanSourcesIncludingTheirRelPaths = array();
foreach ($allEnqueuedCleanSources as $allEnqueuedCleanSource) {
$allEnqueuedCleanSourcesIncludingTheirRelPaths[] = $allEnqueuedCleanSource;
if (strpos($allEnqueuedCleanSource, 'http://') === 0 || strpos($allEnqueuedCleanSource, 'https://') === 0) {
$allEnqueuedCleanSourcesIncludingTheirRelPaths[] = str_replace(array('http://', 'https://'), '//', $allEnqueuedCleanSource);
// e.g. www.mysite.com/blog/
if ($parseSiteUrlPath !== '/' && strlen($parseSiteUrlPath) > 1) {
$allEnqueuedCleanSourcesIncludingTheirRelPaths[] = $parseSiteUrlPath . str_replace(site_url(), '', $allEnqueuedCleanSource);
}
// e.g. www.mysite.com/
if ($parseSiteUrlPath === '/' || ! $parseSiteUrlPath) {
$allEnqueuedCleanSourcesIncludingTheirRelPaths[] = str_replace(site_url(), '', $allEnqueuedCleanSource);
}
}
}
$cdnUrls = OptimizeCommon::getAnyCdnUrls();
$cdnUrlForCss = isset($cdnUrls['css']) ? $cdnUrls['css'] : false;
// Grabs both LINK "stylesheet" and those with as="style" which is for preloaded LINK tags
preg_match_all('#<link[^>]*(stylesheet|(as(\s+|)=(\s+|)(|"|\')style(|"|\')))[^>]*>#Umi', OptimizeCommon::cleanerHtmlSource( $htmlSource, array( 'for_fetching_link_tags' ) ), $matchesSourcesFromTags, PREG_SET_ORDER);
if (empty($matchesSourcesFromTags)) {
return $htmlSource;
}
$cssOptimizeListHardcoded = $linkTagsToUpdate = array();
foreach ($matchesSourcesFromTags as $matches) {
$linkSourceTag = $matches[0];
if ($linkSourceTag === '' || strip_tags($linkSourceTag) !== '') {
// Hmm? Not a valid tag... Skip it...
continue;
}
// Check if the CSS has any 'data-wpacu-skip' attribute; if it does, do not alter it
if (preg_match('#data-wpacu-skip([=>/ ])#i', $linkSourceTag)) {
continue;
}
$cleanLinkHrefFromTagArray = OptimizeCommon::getLocalCleanSourceFromTag($linkSourceTag);
// Skip external links, no point in carrying on
if (! $cleanLinkHrefFromTagArray || ! is_array($cleanLinkHrefFromTagArray)) {
continue;
}
// Is it a local CSS? Check if it's hardcoded (not enqueued the WordPress way)
$cleanLinkHrefFromTag = $cleanLinkHrefFromTagArray['source'];
$afterQuestionMark = $cleanLinkHrefFromTagArray['after_question_mark'];
$isHardcodedDetected = false;
if (! in_array($cleanLinkHrefFromTag, $allEnqueuedCleanSourcesIncludingTheirRelPaths)) {
// Not in the final enqueued list? Most likely hardcoded (not added via wp_enqueue_scripts())
// Emulate the object value (as the enqueued styles)
$generatedHandle = md5($cleanLinkHrefFromTag);
$value = (object)array(
'handle' => $generatedHandle,
'src' => $cleanLinkHrefFromTag,
'ver' => md5($afterQuestionMark)
);
$optimizeValues = self::maybeOptimizeIt($value);
ObjectCache::wpacu_cache_set('wpacu_maybe_optimize_it_css_'.$generatedHandle, $optimizeValues);
if (! empty($optimizeValues)) {
$isHardcodedDetected = true;
$cssOptimizeListHardcoded[] = $optimizeValues;
}
}
if ( ! $isHardcodedDetected ) {
$listToParse = $cssOptimizeList;
} else {
$listToParse = $cssOptimizeListHardcoded;
}
if (empty($listToParse)) {
continue;
}
foreach ($listToParse as $listValues) {
// Index 0: Source URL (relative)
// Index 1: New Optimized URL (relative)
// Index 2: Source URL (as it is)
// if the relative path from the WP root does not match the value of the source from the tag, do not continue
// e.g. '/wp-content/plugins/my-plugin/script.js' has to be inside '<script src="/wp-content/plugins/my-plugin/script.js?ver=1.1"></script>'
if (strpos($cleanLinkHrefFromTag, $listValues[0]) === false) {
continue;
}
// The contents of the CSS file has been changed and thus, we will replace the source path from the original tag with the cached (e.g. minified) one
// If the minified files are deleted (e.g. /wp-content/cache/ is cleared)
// do not replace the CSS file path to avoid breaking the website
$localPathOptimizedFile = rtrim(Misc::getWpRootDirPath(), '/') . $listValues[1];
if (! is_file($localPathOptimizedFile)) {
continue;
}
// Make sure the source URL gets updated even if it starts with // (some plugins/theme strip the protocol when enqueuing assets)
// If the first value fails to be replaced, the next one will be attempted for replacement
// the order of the elements in the array is very important
$sourceUrlList = array(
site_url() . $listValues[0], // with protocol
$siteUrlNoProtocol . $listValues[0], // without protocol
);
if ($parseSiteUrlPath && (strpos($listValues[0], $parseSiteUrlPath) === 0 || strpos($cleanLinkHrefFromTag, $parseSiteUrlPath) === 0)) {
$sourceUrlList[] = $cleanLinkHrefFromTag;
}
if ($parseSiteUrlPath && (strpos($cleanLinkHrefFromTag, $parseSiteUrlPath) === 0 && strpos($cleanLinkHrefFromTag, $listValues[0]) !== false)) {
$sourceUrlList[] = str_replace('//', '/', $parseSiteUrlPath.'/'.$listValues[0]);
}
elseif ( $cleanLinkHrefFromTag === $listValues[0] ) {
$sourceUrlList[] = $listValues[0];
}
if ($cdnUrlForCss) {
// Does it have a CDN?
$sourceUrlList[] = OptimizeCommon::cdnToUrlFormat($cdnUrlForCss, 'rel') . $listValues[0];
}
// Any rel tag? You never know
// e.g. <link src="/wp-content/themes/my-theme/style.css"></script>
if ( (strpos($listValues[2], '/') === 0 && strpos($listValues[2], '//') !== 0)
|| (strpos($listValues[2], '/') !== 0 &&
strpos($listValues[2], '//') !== 0 &&
stripos($listValues[2], 'http://') !== 0 &&
stripos($listValues[2], 'https://') !== 0) ) {
$sourceUrlList[] = $listValues[2];
}
if ( $cleanLinkHrefFromTag === $listValues[0] ) {
$sourceUrlList[] = $cleanLinkHrefFromTag;
}
// If no CDN is set, it will return site_url() as a prefix
$optimizeUrl = OptimizeCommon::cdnToUrlFormat($cdnUrlForCss, 'raw') . $listValues[1]; // string
if ($linkSourceTag !== str_replace($sourceUrlList, $optimizeUrl, $linkSourceTag)) {
// Extra measure: Check the file size which should be 4 bytes, but add some margin error in case some environments will report less
$isEmptyOptimizedFile = (strpos($localPathOptimizedFile, '-wpacu-empty-file.css') !== false && filesize($localPathOptimizedFile) < 10);
// Strip it as its content (after optimization, for instance) is empty; no point in having extra HTTP requests
if ($isEmptyOptimizedFile) {
// Note: As for September 3, 2020, the inline CSS associated with the handle is no longer removed if the main CSS file is empty
// There could be cases when the main CSS file is empty (e.g. theme's styling), but the inline STYLE tag associated with it has syntax that is needed
$htmlSource = str_replace($linkSourceTag, '', $htmlSource);
} else {
// Do the replacement
$newLinkSourceTag = self::updateOriginalToOptimizedTag( $linkSourceTag, $sourceUrlList, $optimizeUrl );
$linkTagsToUpdate[$linkSourceTag] = $newLinkSourceTag;
}
break; // there was a match, stop here
}
}
}
return strtr($htmlSource, $linkTagsToUpdate);
}
/**
* @param $linkSourceTag string
* @param $sourceUrlList array
* @param $optimizeUrl string
*
* @return array|string|string[]|null
*/
public static function updateOriginalToOptimizedTag($linkSourceTag, $sourceUrlList, $optimizeUrl)
{
if (is_array($sourceUrlList) && ! empty($sourceUrlList)) {
foreach ($sourceUrlList as $sourceUrl) {
$newLinkSourceTag = str_replace($sourceUrl, $optimizeUrl, $linkSourceTag);
if ($newLinkSourceTag !== $linkSourceTag) {
break;
}
}
} else {
$newLinkSourceTag = str_replace( $sourceUrlList, $optimizeUrl, $linkSourceTag );
}
// Needed in case it's added to the Combine CSS exceptions list
if (CombineCss::proceedWithCssCombine()) {
$sourceUrlRel = is_array($sourceUrlList) ? OptimizeCommon::getSourceRelPath($sourceUrlList[0]) : OptimizeCommon::getSourceRelPath($sourceUrlList);
$newLinkSourceTag = str_ireplace('<link ', '<link data-wpacu-link-rel-href-before="'.$sourceUrlRel.'" ', $newLinkSourceTag);
}
$hrefValue = Misc::getValueFromTag($newLinkSourceTag);
// No space from the matching and ? should be there
if ($hrefValue && ( strpos( $hrefValue, ' ' ) === false )) {
if ( strpos( $hrefValue, '?' ) !== false ) {
// Strip things like ?ver=
list( , $toStrip ) = explode( '?', $hrefValue );
$toStrip = '?' . trim( $toStrip );
$newLinkSourceTag = str_replace( $toStrip, '', $newLinkSourceTag );
}
if ( strpos( $hrefValue, '&#038;ver' ) !== false ) {
// Replace any .js&#038;ver with .js
$toStrip = strrchr($hrefValue, '&#038;ver');
$newLinkSourceTag = str_replace( $toStrip, '', $newLinkSourceTag );
}
}
global $wp_version;
$newLinkSourceTag = str_replace('.css&#038;ver='.$wp_version, '.css', $newLinkSourceTag);
$newLinkSourceTag = str_replace('.css&#038;ver=', '.css', $newLinkSourceTag);
return preg_replace('!\s+!', ' ', $newLinkSourceTag); // replace multiple spaces with only one space
}
/**
* @return bool
*/
public static function isInlineCssEnabled()
{
$isEnabledInSettingsWithListOrAuto = (Main::instance()->settings['inline_css_files'] &&
(trim(Main::instance()->settings['inline_css_files_list']) !== '' || self::isAutoInlineEnabled()));
if (! $isEnabledInSettingsWithListOrAuto) {
return false;
}
// Deactivate it for debugging purposes via query string /?wpacu_no_inline_js
if ( isset($_GET['wpacu_no_inline_css']) ) {
return false;
}
// Finally, return true
return true;
}
/**
* From LINK to STYLE tag: it processes the contents of the LINK stylesheet and replaces the tag with a STYLE tag having the content inlined
*
* @param $htmlSource
*
* @return mixed
*/
public static function doInline($htmlSource)
{
$allPatterns = self::getAllInlineChosenPatterns();
// Skip any LINK tags within conditional comments (e.g. Internet Explorer ones)
preg_match_all(
'#<link[^>]*stylesheet[^>]*>#Umsi',
OptimizeCommon::cleanerHtmlSource( $htmlSource, array( 'strip_content_between_conditional_comments', 'for_fetching_link_tags' ) ),
$matchesSourcesFromTags,
PREG_SET_ORDER
);
// In case automatic inlining is used
$belowSizeInput = (int)Main::instance()->settings['inline_css_files_below_size_input'];
if ($belowSizeInput === 0) {
$belowSizeInput = 1; // needs to have a minimum value
}
if (! empty($matchesSourcesFromTags)) {
$cdnUrls = OptimizeCommon::getAnyCdnUrls();
$cdnUrlForCss = isset($cdnUrls['css']) ? trim($cdnUrls['css']) : false;
foreach ($matchesSourcesFromTags as $matchList) {
$matchedTag = $matchList[0];
if ( stripos( $matchedTag, '<link' ) !== 0 ) {
continue;
}
// Do not inline the admin bar SCRIPT file, saving resources as it's shown for the logged-in user only
if (strpos($matchedTag, '/wp-includes/css/admin-bar') !== false) {
continue;
}
// They were preloaded for a reason, leave them
if (strpos($matchedTag, 'data-wpacu-preload-it-async=') !== false || strpos($matchedTag, 'data-wpacu-to-be-preloaded-basic=') !== false) {
continue;
}
if (strip_tags($matchedTag) !== '') {
continue; // something is funny, don't mess with the HTML alteration, leave it as it was
}
$chosenInlineCssMatches = false;
// Condition #1: Only chosen (via textarea) CSS get inlined
if ( false !== strpos( $matchedTag, ' wpacu-to-be-inlined' ) ) {
$chosenInlineCssMatches = true;
} elseif ( ! empty( $allPatterns ) ) {
// Fallback, in case "wpacu-to-be-inlined" was not already added to the tag
foreach ($allPatterns as $patternToCheck) {
if (preg_match('#'.$patternToCheck.'#si', $matchedTag) || strpos($matchedTag, $patternToCheck) !== false) {
$chosenInlineCssMatches = true;
break;
}
}
}
// Is auto inline disabled and the chosen CSS does not match? Continue to the next LINK tag
if (! $chosenInlineCssMatches && ! self::isAutoInlineEnabled()) {
continue;
}
$linkHrefOriginal = Misc::getValueFromTag($matchedTag);
$localAssetPath = OptimizeCommon::getLocalAssetPath($linkHrefOriginal, 'css');
if (! $localAssetPath) {
continue; // Not on the same domain
}
// Condition #2: Auto inline is enabled and there's no match for any entry in the textarea
if (! $chosenInlineCssMatches && self::isAutoInlineEnabled()) {
$fileSizeKb = number_format(filesize($localAssetPath) / 1024, 2);
// If it's not smaller than the value from the input, do not continue with the inlining
if ($fileSizeKb >= $belowSizeInput) {
continue;
}
}
// Is there a media attribute? Make sure to add it to the STYLE tag
$mediaAttrValue = Misc::getValueFromTag($matchedTag, 'media');
$mediaAttr = ($mediaAttrValue && $mediaAttrValue !== 'all') ? 'media=\''.$mediaAttrValue.'\'' : '';
$appendBeforeAnyRelPath = $cdnUrlForCss ? OptimizeCommon::cdnToUrlFormat($cdnUrlForCss, 'raw') : '';
$cssContent = self::maybeFixCssContent(
FileSystem::fileGetContents($localAssetPath, 'combine_css_imports'), // CSS content
$appendBeforeAnyRelPath . OptimizeCommon::getPathToAssetDir($linkHrefOriginal) . '/'
);
// The CSS file is read from its original plugin/theme/cache location
// If minify was enabled, then it's already minified, no point in re-minify it to save resources
// Changing paths (relative) to fonts, images, etc. are relevant in this case
$cssContent = self::maybeAlterContentForCssFile($cssContent, false);
if ($cssContent && $cssContent !== '/**/') {
$htmlSource = str_replace(
$matchedTag,
'<style '.Misc::getStyleTypeAttribute().' '.$mediaAttr.' data-wpacu-inline-css-file=\'1\'>'."\n".$cssContent."\n".'</style>',
$htmlSource
);
} else {
// After CSS alteration (e.g. minify), there's no content left, most likely the CSS file contained only comments, elements without any syntax or empty spaces
// Strip the tag completely as there's no reason to print an empty SCRIPT tag to further add to the total DOM elements
$htmlSource = str_replace($matchedTag, '', $htmlSource);
}
}
}
return $htmlSource;
}
/**
* This applies to both inline and static JS files contents
*
* @param $cssContent
* @param bool $doCssMinify (false by default as it could be already minified or non-minify type)
* @param array $extraParams
*
* @return mixed|string|string[]|null
*/
public static function maybeAlterContentForCssFile($cssContent, $doCssMinify = false, $extraParams = array())
{
if (! trim($cssContent)) {
return $cssContent;
}
/* [START] Change CSS Content */
// Move any @imports to top; This also strips any @imports to Google Fonts if the option is chosen
$cssContent = self::importsUpdate( $cssContent );
if ( $doCssMinify ) {
$cssContent = MinifyCss::applyMinification( $cssContent, $doCssMinify );
}
if ( Main::instance()->settings['google_fonts_remove'] ) {
$cssContent = FontsGoogleRemove::cleanFontFaceReferences( $cssContent );
}
// Does it have a source map? Strip it
if (strpos($cssContent, '/*# sourceMappingURL=') !== false) {
$cssContent = OptimizeCommon::stripSourceMap($cssContent, 'css');
}
/* [END] Change CSS Content */
return $cssContent;
}
/**
* @param $cssContent
* @param bool $doCssMinify
* @param array $extraParams
*
* @return mixed|string
*/
public static function maybeAlterContentForInlineStyleTag($cssContent, $doCssMinify = false, $extraParams = array())
{
if (! trim($cssContent)) {
return $cssContent;
}
$useCacheForInlineStyle = true;
if (mb_strlen($cssContent) > 500000) { // Bigger then ~500KB? Skip alteration
return $cssContent;
}
if (mb_strlen($cssContent) < 40000) { // Smaller than ~40KB? Do not cache it
$useCacheForInlineStyle = false;
}
// For debugging purposes
if (isset($_GET['wpacu_no_cache']) || (defined('WPACU_NO_CACHE') && WPACU_NO_CACHE === true)) { $useCacheForInlineStyle = false; }
if ($useCacheForInlineStyle) {
// Anything in the cache? Take it from there and don't spend resources with the minification
// (which in some environments uses the CPU, depending on the complexity of the JavaScript code) and any other alteration
$cssContentBeforeHash = sha1( $cssContent );
$pathToInlineCssOptimizedItem = WP_CONTENT_DIR . self::getRelPathCssCacheDir() . '/item/inline/' . $cssContentBeforeHash . '.css';
// Check if the file exists before moving forward
if ( is_file( $pathToInlineCssOptimizedItem ) ) {
$cachedCssFileExpiresIn = OptimizeCommon::$cachedAssetFileExpiresIn;
if ( filemtime( $pathToInlineCssOptimizedItem ) < ( time() - 1 * $cachedCssFileExpiresIn ) ) {
// Has the caching period expired? Remove the file as a new one has to be generated
@unlink( $pathToInlineCssOptimizedItem );
} else {
// Not expired / Return its content from the cache in a faster way
$inlineCssStorageItemJsonContent = trim( FileSystem::fileGetContents( $pathToInlineCssOptimizedItem ) );
if ( $inlineCssStorageItemJsonContent !== '' ) {
return $inlineCssStorageItemJsonContent;
}
}
}
}
/* [START] Change CSS Content */
if ( $doCssMinify && in_array('just_minify', $extraParams) ) {
$cssContent = MinifyCss::applyMinification( $cssContent, $useCacheForInlineStyle );
} else {
// Move any @imports to top; This also strips any @imports to Google Fonts if the option is chosen
$cssContent = self::importsUpdate( $cssContent );
if ( $doCssMinify ) {
$cssContent = MinifyCss::applyMinification( $cssContent, $useCacheForInlineStyle );
}
if ( Main::instance()->settings['google_fonts_remove'] ) {
$cssContent = FontsGoogleRemove::cleanFontFaceReferences( $cssContent );
}
}
/* [END] Change CSS Content */
if ($useCacheForInlineStyle && isset($pathToInlineCssOptimizedItem)) {
// Store the optimized content to the cached CSS file which would be read quicker
FileSystem::filePutContents( $pathToInlineCssOptimizedItem, $cssContent );
}
return $cssContent;
}
/**
* @return bool
*/
public static function isAutoInlineEnabled()
{
return Main::instance()->settings['inline_css_files'] &&
Main::instance()->settings['inline_css_files_below_size'] &&
(int)Main::instance()->settings['inline_css_files_below_size_input'] > 0;
}
/**
* Source: https://www.minifier.org/ | https://github.com/matthiasmullie/minify
*
* @param $content
*
* @return string
*/
public static function importsUpdate($content)
{
if (preg_match_all('/(;?)(@import (?<url>url\()?(?P<quotes>["\']?).+?(?P=quotes)(?(url)\)));?/', $content, $matches)) {
// Remove from content (they will be appended to the top if they qualify)
foreach ($matches[0] as $import) {
$content = str_replace($import, '', $content);
}
// Strip any @imports to Google Fonts if it's the case
$importsAddToTop = Main::instance()->settings['google_fonts_remove'] ? FontsGoogleRemove::stripGoogleApisImport($matches[2]) : $matches[2];
// Add to top if there are any imports left
if (! empty($importsAddToTop)) {
$content = implode(';', $importsAddToTop) . ';' . trim($content, ';');
}
}
return $content;
}
/**
* e.g. if a style is unloaded, strip any LINK tag that preloads that style (e.g. added by other plugins)
*
* @param $htmlSource
*
* @return array|mixed|string|string[]
*/
public static function stripAnyReferencesForUnloadedStyles($htmlSource)
{
// Gather all HREFs of the unloaded styles (if any)
$unloadedStyleRelHrefs = array();
if ( isset( Main::instance()->allUnloadedAssets['styles'] ) && ! empty( Main::instance()->allUnloadedAssets['styles'] ) ) {
foreach ( array_unique( Main::instance()->allUnloadedAssets['styles'] ) as $styleHandle ) {
if ( ! (isset(Main::instance()->wpAllStyles['registered'][ $styleHandle ]->src) && Main::instance()->wpAllStyles['registered'][ $styleHandle ]->src) ) {
continue; // does not have a "src" (e.g. inline CSS)
}
$unloadedStyleRelHrefs[] = OptimizeCommon::getSourceRelPath( Main::instance()->wpAllStyles['registered'][ $styleHandle ]->src );
}
}
if ( ! empty($unloadedStyleRelHrefs) ) {
$htmlSource = OptimizeCommon::matchAndReplaceLinkTags($htmlSource, array('as' => 'style', 'unloaded_assets_rel_sources' => $unloadedStyleRelHrefs));
}
return $htmlSource;
}
/**
* @param string $returnType
*
* @return array|bool
*/
public static function isOptimizeCssEnabledByOtherParty($returnType = 'list')
{
$pluginsToCheck = array(
'autoptimize/autoptimize.php' => 'Autoptimize',
'wp-rocket/wp-rocket.php' => 'WP Rocket',
'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache',
'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache',
'sg-cachepress/sg-cachepress.php' => 'SG Optimizer',
'fast-velocity-minify/fvm.php' => 'Fast Velocity Minify',
'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache',
'swift-performance-lite/performance.php' => 'Swift Performance Lite',
'breeze/breeze.php' => 'Breeze WordPress Cache Plugin'
);
$cssOptimizeEnabledIn = array();
foreach ($pluginsToCheck as $plugin => $pluginTitle) {
// "Autoptimize" check
if ($plugin === 'autoptimize/autoptimize.php' && Misc::isPluginActive($plugin) && get_option('autoptimize_css')) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
// "WP Rocket" check
if ($plugin === 'wp-rocket/wp-rocket.php' && Misc::isPluginActive($plugin)) {
if (function_exists('get_rocket_option')) {
$wpRocketMinifyCss = trim(get_rocket_option('minify_css')) ?: false;
$wpRocketMinifyConcatenateCss = trim(get_rocket_option('minify_concatenate_css')) ?: false;
} else {
$wpRocketSettings = get_option('wp_rocket_settings');
$wpRocketMinifyCss = isset($wpRocketSettings['minify_css']) && trim($wpRocketSettings['minify_css']);
$wpRocketMinifyConcatenateCss = isset($wpRocketSettings['minify_concatenate_css']) && trim($wpRocketSettings['minify_concatenate_css']);
}
if ($wpRocketMinifyCss || $wpRocketMinifyConcatenateCss) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
// "WP Fastest Cache" check
if ($plugin === 'wp-fastest-cache/wpFastestCache.php' && Misc::isPluginActive($plugin)) {
$wpfcOptionsJson = get_option('WpFastestCache');
$wpfcOptions = @json_decode($wpfcOptionsJson, ARRAY_A);
if (isset($wpfcOptions['wpFastestCacheMinifyCss']) || isset($wpfcOptions['wpFastestCacheCombineCss'])) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
// "W3 Total Cache" check
if ($plugin === 'w3-total-cache/w3-total-cache.php' && Misc::isPluginActive($plugin)) {
$w3tcConfigMaster = Misc::getW3tcMasterConfig();
$w3tcEnableCss = (int)trim(Misc::extractBetween($w3tcConfigMaster, '"minify.css.enable":', ','), '" ');
if ($w3tcEnableCss === 1) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
// "SG Optimizer" check
if ($plugin === 'sg-cachepress/sg-cachepress.php' && Misc::isPluginActive($plugin)) {
if (class_exists('\SiteGround_Optimizer\Options\Options')
&& method_exists('\SiteGround_Optimizer\Options\Options', 'is_enabled')
&& @\SiteGround_Optimizer\Options\Options::is_enabled('siteground_optimizer_combine_css')) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
// "Fast Velocity Minify" check
if ($plugin === 'fast-velocity-minify/fvm.php' && Misc::isPluginActive($plugin)) {
// It's enough if it's active due to its configuration
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
// "LiteSpeed Cache" check
if ($plugin === 'litespeed-cache/litespeed-cache.php' && Misc::isPluginActive($plugin) && ($liteSpeedCacheConf = apply_filters('litespeed_cache_get_options', get_option('litespeed-cache-conf')))) {
if ( (isset($liteSpeedCacheConf['css_minify']) && $liteSpeedCacheConf['css_minify'])
|| (isset($liteSpeedCacheConf['css_combine']) && $liteSpeedCacheConf['css_combine']) ) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
// "Swift Performance Lite" check
if ($plugin === 'swift-performance-lite/performance.php' && Misc::isPluginActive($plugin)
&& class_exists('Swift_Performance_Lite') && method_exists('Swift_Performance_Lite', 'check_option')) {
if ( @\Swift_Performance_Lite::check_option('merge-styles', 1) ) {
$cssOptimizeEnabledIn[] = $pluginTitle;
}
if ($returnType === 'if_enabled') { return true; }
}
// "Breeze WordPress Cache Plugin"
if ($plugin === 'breeze/breeze.php' && Misc::isPluginActive($plugin)) {
$breezeBasicSettings = get_option('breeze_basic_settings');
$breezeAdvancedSettings = get_option('breeze_advanced_settings');
if (isset($breezeBasicSettings['breeze-minify-css'], $breezeAdvancedSettings['breeze-group-css'])
&& $breezeBasicSettings['breeze-minify-css'] && $breezeAdvancedSettings['breeze-group-css']) {
$cssOptimizeEnabledIn[] = $pluginTitle;
if ($returnType === 'if_enabled') { return true; }
}
}
}
if ($returnType === 'if_enabled') { return false; }
return $cssOptimizeEnabledIn;
}
/**
* @return bool
*/
public static function isWpRocketOptimizeCssDeliveryEnabled()
{
if (Misc::isPluginActive('wp-rocket/wp-rocket.php')) {
if (function_exists('get_rocket_option')) {
$wpRocketAsyncCss = trim(get_rocket_option('async_css')) ?: false;
} else {
$wpRocketSettings = get_option('wp_rocket_settings');
$wpRocketAsyncCss = isset($wpRocketSettings['async_css']) && trim($wpRocketSettings['async_css']);
}
return $wpRocketAsyncCss;
}
return false;
}
/**
* @return bool
*/
public static function wpfcMinifyCssEnabledOnly()
{
if (Misc::isPluginActive('wp-fastest-cache/wpFastestCache.php')) {
$wpfcOptionsJson = get_option('WpFastestCache');
$wpfcOptions = @json_decode($wpfcOptionsJson, ARRAY_A);
// "Minify CSS" is enabled, "Combine CSS" is disabled
return isset($wpfcOptions['wpFastestCacheMinifyCss']) && ! isset($wpfcOptions['wpFastestCacheCombineCss']);
}
return false;
}
/**
* @return bool
*/
public static function isWorthCheckingForOptimization()
{
// At least one of these options have to be enabled
// Otherwise, we will not perform specific useless actions and save resources
return MinifyCss::isMinifyCssEnabled() ||
Main::instance()->settings['google_fonts_display'] ||
Main::instance()->settings['google_fonts_remove'];
}
/**
* @param $htmlSource
*
* @return mixed
*/
public static function ignoreDependencyRuleAndKeepChildrenLoaded($htmlSource)
{
$ignoreChild = Main::instance()->getIgnoreChildren();
if (isset($ignoreChild['styles']) && ! empty($ignoreChild['styles'])) {
foreach (array_keys($ignoreChild['styles']) as $styleHandle) {
// Always load the Dashicons if the top admin bar (toolbar) is shown
if ($styleHandle === 'dashicons' && is_admin_bar_showing()) {
continue;
}
if (isset(Main::instance()->wpAllStyles['registered'][$styleHandle]->src, Main::instance()->ignoreChildren['styles'][$styleHandle.'_has_unload_rule']) && Main::instance()->wpAllStyles['registered'][$styleHandle]->src && Main::instance()->ignoreChildren['styles'][$styleHandle.'_has_unload_rule']) {
if ($scriptExtraAfterHtml = self::generateInlineAssocHtmlForHandle($styleHandle)) {
$htmlSource = str_replace($scriptExtraAfterHtml, '', $htmlSource);
}
$listWithMatches = array();
$listWithMatches[] = 'data-wpacu-style-handle=[\'"]'.$styleHandle.'[\'"]';
if ($styleSrc = Main::instance()->wpAllStyles['registered'][$styleHandle]->src) {
$listWithMatches[] = OptimizeCommon::getSourceRelPath($styleSrc);
}
$htmlSource = CleanUp::cleanLinkTagFromHtmlSource($listWithMatches, $htmlSource);
}
}
}
return $htmlSource;
}
/**
* @param $styleTagOrHandle
* @param $wpacuRegisteredStyles
* @param $from
* @param string $return ("value": CSS Inline Content / "html": CSS Inline Content surrounded by tags)
*
* @return array
*/
public static function getInlineAssociatedWithLinkHandle($styleTagOrHandle, $wpacuRegisteredStyles, $from = 'tag', $return = 'value')
{
$styleExtraAfter = '';
if ($from === 'tag') {
preg_match_all('#data-wpacu-style-handle=([\'])' . '(.*)' . '(\1)#Usmi', $styleTagOrHandle, $outputMatches);
$styleHandle = (isset($outputMatches[2][0]) && $outputMatches[2][0]) ? trim($outputMatches[2][0], '"\'') : '';
} else {
$styleHandle = $styleTagOrHandle;
}
if ($return === 'value' && $styleHandle && isset($wpacuRegisteredStyles[$styleHandle]->extra)) {
$styleExtraArray = $wpacuRegisteredStyles[$styleHandle]->extra;
if (isset($styleExtraArray['after']) && ! empty($styleExtraArray['after'])) {
$styleExtraAfter .= "<style id='".$styleHandle."-inline-css' ".Misc::getStyleTypeAttribute().">\n";
foreach ($styleExtraArray['after'] as $afterData) {
if (! is_bool($afterData)) {
$styleExtraAfter .= $afterData."\n";
}
}
$styleExtraAfter .= '</style>';
}
return array('after' => $styleExtraAfter);
}
if ( $return === 'html' && $styleHandle ) {
// 'after' is the only one for inline CSS; there's no 'data' or 'before' like in the inline JS
return array('after' => self::generateInlineAssocHtmlForHandle($styleHandle));
}
return array('after' => array());
}
/**
* @param $handle
* @param $inlineStyleContent
*
* @return string
*/
public static function generateInlineAssocHtmlForHandle($handle, $inlineStyleContent = '')
{
global $wp_styles;
if ( ! $inlineStyleContent ) {
$inlineStyleContent = $wp_styles->print_inline_style( $handle, false );
}
$output = '';
if ( $inlineStyleContent ) {
$output = sprintf(
"<style id='%s-inline-css'%s>\n%s\n</style>",
esc_attr( $handle ),
Misc::getStyleTypeAttribute(),
$inlineStyleContent
);
}
return $output;
}
/**
* @param $htmlSource
*
* @return array|string|string[]
*/
public function appendNoScriptCertainLinkTags($htmlSource)
{
preg_match_all('#<link[^>]*(data-wpacu-preload-it-async)[^>]*(>)#Umi', $htmlSource, $matchesSourcesFromTags, PREG_SET_ORDER);
$noScripts = '';
if (! empty($matchesSourcesFromTags)) {
foreach ($matchesSourcesFromTags as $matchedValues) {
$matchedTag = $matchedValues[0];
$mediaAttrValue = Misc::getValueFromTag($matchedTag, 'media');
$hrefAttrValue = Misc::getValueFromTag($matchedTag);
$noScripts .= '<noscript><link rel="stylesheet" href="'.$hrefAttrValue.'" media="'.$mediaAttrValue.'" /></noscript>'."\n";
}
}
return str_replace(self::MOVE_NOSCRIPT_TO_BODY_FOR_CERTAIN_LINK_TAGS, $noScripts, $htmlSource);
}
}