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

1537 lines
55 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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);
}
}