done) && is_array($wp_scripts->done) ? $wp_scripts->done : array(); $wpScriptsQueue = isset($wp_scripts->queue) && is_array($wp_scripts->queue) ? $wp_scripts->queue : array(); $wpScriptsList = array_unique(array_merge($wpScriptsDone, $wpScriptsQueue)); // Collect all enqueued clean (no query strings) HREFs to later compare them against any hardcoded JS $allEnqueuedCleanScriptSrcs = array(); // [Start] Collect for caching if ( ! empty($wpScriptsList) ) { $isMinifyJsFilesEnabled = MinifyJs::isMinifyJsEnabled() && in_array(Main::instance()->settings['minify_loaded_js_for'], array('src', 'all', '')); foreach ( $wpScriptsList as $index => $scriptHandle ) { if ( isset( Main::instance()->wpAllScripts['registered'][ $scriptHandle ]->src ) && ( $src = Main::instance()->wpAllScripts['registered'][ $scriptHandle ]->src ) ) { $localAssetPath = OptimizeCommon::getLocalAssetPath( $src, 'js' ); if ( ! $localAssetPath || ! is_file( $localAssetPath ) ) { continue; // not a local file } ob_start(); $wp_scripts->do_item( $scriptHandle ); $scriptSourceTag = trim( ob_get_clean() ); // Check if the JS has any 'data-wpacu-skip' attribute; if it does, do not alter it if ( preg_match( '#data-wpacu-skip([=>/ ])#i', $scriptSourceTag ) ) { unset( $wpScriptsList[ $index ] ); continue; } $cleanScriptSrcFromTagArray = OptimizeCommon::getLocalCleanSourceFromTag( $scriptSourceTag ); if ( isset( $cleanScriptSrcFromTagArray['source'] ) && $cleanScriptSrcFromTagArray['source'] ) { $allEnqueuedCleanScriptSrcs[] = $cleanScriptSrcFromTagArray['source']; } $optimizeValues = self::maybeOptimizeIt( Main::instance()->wpAllScripts['registered'][ $scriptHandle ], array( 'local_asset_path' => $localAssetPath, 'is_minify_js_enabled' => $isMinifyJsFilesEnabled ) ); ObjectCache::wpacu_cache_set( 'wpacu_maybe_optimize_it_js_' . $scriptHandle, $optimizeValues ); if ( ! empty( $optimizeValues ) ) { $jsOptimizeList[] = $optimizeValues; } } } } ObjectCache::wpacu_cache_add('wpacu_js_enqueued_srcs', $allEnqueuedCleanScriptSrcs); ObjectCache::wpacu_cache_add('wpacu_js_optimize_list', $jsOptimizeList); // [End] Collect for caching } /** * @param $value * @param array $fileAlreadyChecked * * @return array */ public static function maybeOptimizeIt($value, $fileAlreadyChecked = array()) { if ($optimizeValues = ObjectCache::wpacu_cache_get('wpacu_maybe_optimize_it_js_'.$value->handle)) { return $optimizeValues; } global $wp_version; $src = isset($value->src) ? $value->src : false; if (! $src) { return array(); } $doFileMinify = true; $isMinifyJsFilesEnabled = (isset($fileAlreadyChecked['is_minify_js_enabled']) && $fileAlreadyChecked['is_minify_js_enabled']) ? $fileAlreadyChecked['is_minify_js_enabled'] : MinifyJs::isMinifyJsEnabled() && in_array(Main::instance()->settings['minify_loaded_js_for'], array('src', 'all', '')); if ( ! $isMinifyJsFilesEnabled || MinifyJs::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; $isJsFile = 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, 'js' ); $checkCond = $localAssetPath && is_file($localAssetPath); } if ($checkCond) { if ($fileMTime = @filemtime($localAssetPath)) { $dbVer = $fileMTime; } $isJsFile = true; } if ($isJsFile) { // 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_js_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-added to the database with the new version OptimizeCommon::deleteTransient($transientName); } else { $localPathToJsOptimized = str_replace( '//', '/', Misc::getWpRootDirPath() . $savedValuesArray['optimize_uri'] ); // Do not load any minified JS file (from the database transient cache) if it doesn't exist // It will fallback to the original JS file if ( isset( $savedValuesArray['source_uri'] ) && is_file( $localPathToJsOptimized ) ) { 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 ); } } } } // Check if it starts without "/" or a protocol; e.g. "wp-content/theme/script.js" 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; } /* * [START] JS Content Optimization */ if (Main::instance()->settings['cache_dynamic_loaded_js'] && ((strpos($src, '/?') !== false) || strpos($src, '.php?') !== false || Misc::endsWith($src, '.php')) && (strpos($src, site_url()) !== false) ) { $pathToAssetDir = ''; $sourceBeforeOptimization = $value->src; if (! ($jsContent = DynamicLoadedAssets::getAssetContentFrom('dynamic', $value))) { return array(); } } else { if (! $isJsFile) { return array(); } /* * This is a local .JS file */ $pathToAssetDir = OptimizeCommon::getPathToAssetDir($value->src); $sourceBeforeOptimization = str_replace(Misc::getWpRootDirPath(), '/', $localAssetPath); $jsContent = FileSystem::fileGetContents($localAssetPath); } $hadToBeMinified = false; $jsContent = trim($jsContent); // If it stays like this, it means there is content there, even if only comments $jsContentBecomesEmptyAfterMin = false; if ( $doFileMinify && $jsContent ) { // only bother to minify it if it has any content, save resources // Minify this file? $jsContentBeforeMin = $jsContent; $jsContentAfterMin = MinifyJs::applyMinification($jsContentBeforeMin); $jsContent = $jsContentAfterMin; if ( $jsContentBeforeMin && $jsContentAfterMin === '' ) { // It had content, but became empty after minification, most likely it had only comments (e.g. a default child theme's style) $jsContentBecomesEmptyAfterMin = true; } else { $jsContentCompare = md5(trim( $jsContentBeforeMin, '; ' )); $jsContentCompareWith = md5(trim( $jsContentAfterMin, '; ' )); if ( $jsContentCompare !== $jsContentCompareWith ) { $hadToBeMinified = true; } } } if ( $jsContentBecomesEmptyAfterMin || $jsContent === '' ) { $jsContent = '/**/'; } else { $jsContentArray = self::maybeAlterContentForJsFile( $jsContent, false ); $jsContent = $jsContentArray['content']; // resulting content after alteration $jsContentAfterAlterToCompare = $jsContentArray['content_after_alter_to_compare']; if ( $isJsFile && ( ! $hadToBeMinified ) ) { $jsContentCompare = md5(trim( $jsContent, '; ' )); $jsContentCompareWith = md5(trim( $jsContentAfterAlterToCompare, '; ' )); if ( $jsContentCompare === $jsContentCompareWith ) { // 1: The file was not minified // 2: It doesn't need any alteration (e.g. no Google Fonts to strip from its content) // No need to copy it in to the cache (save disk space) return array(); } } // Change the necessary relative paths before the file is copied to the caching directory (e.g. /wp-content/cache/asset-cleanup/) $jsContent = self::maybeDoJsFixes( $jsContent, $pathToAssetDir . '/' ); } /* * [END] JS Content Optimization */ if (isset($jsContentBeforeMin) && $jsContent === '/**/' && strpos($jsContentBeforeMin, '/*@cc_on') !== false && strpos($jsContentBeforeMin, '@*/') !== false) { return array(); // Internet Explorer things, leave the file as it is } // Relative path to the new file // Save it to /wp-content/cache/js/{OptimizeCommon::$optimizedSingleFilesDir}/ $fileVer = sha1($jsContent); $uniqueCachedAssetName = OptimizeCommon::generateUniqueNameForCachedAsset($isJsFile, $localAssetPath, $value->handle, $fileVer); $newFilePathUri = self::getRelPathJsCacheDir() . OptimizeCommon::$optimizedSingleFilesDir . '/' . $uniqueCachedAssetName; $newFilePathUri .= '.js'; if ($jsContent === '') { $jsContent = '/**/'; } if ($jsContent === '/**/') { // 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('.js', '-wpacu-empty-file.js', $newFilePathUri); } $newLocalPath = WP_CONTENT_DIR . $newFilePathUri; // Ful Local path $newLocalPathUrl = WP_CONTENT_URL . $newFilePathUri; // Full URL path if ($jsContent && $jsContent !== '/**/' && apply_filters('wpacu_print_info_comments_in_cached_assets', true)) { $jsContent = '/*!' . $sourceBeforeOptimization . '*/' . "\n" . $jsContent; } $saveFile = FileSystem::filePutContents($newLocalPath, $jsContent); if (! $saveFile || ! $jsContent) { // Fallback to the original JS if the optimized version can't be created or updated return array(); } $saveValues = array( 'source_uri' => OptimizeCommon::getSourceRelPath($value->src), 'optimize_uri' => OptimizeCommon::getSourceRelPath($newLocalPathUrl), 'ver' => $dbVer ); // Add / Re-add (with new version) transient OptimizeCommon::setTransient($transientName, wp_json_encode($saveValues)); return array( OptimizeCommon::getSourceRelPath($value->src), // Original SRC (Relative path) OptimizeCommon::getSourceRelPath($newLocalPathUrl), // New SRC (Relative path) $value->src, // SRC (as it is) $value->handle ); } /** * This applies to both inline and static JS files contents * * @param $jsContent * @param bool $doJsMinify (false by default as it could be already minified or non-minify type) * * @return array */ public static function maybeAlterContentForJsFile($jsContent, $doJsMinify = false) { if (! trim($jsContent)) { // No Content! Return it as it, no point in doing extra checks return array('content' => $jsContent); } $jsContentBefore = $jsContent; /* [START] Change JS Content */ if ($doJsMinify) { $jsContent = MinifyJs::applyMinification($jsContent); } if (Main::instance()->settings['google_fonts_remove']) { $jsContent = FontsGoogleRemove::stripReferencesFromJsCode($jsContent); } elseif (Main::instance()->settings['google_fonts_display']) { // Perhaps "display" parameter has to be applied to Google Font Links if they are active $jsContent = FontsGoogle::alterGoogleFontUrlFromJsContent($jsContent); } /* [END] Change JS Content */ // Does it have a source map? Strip it only if any optimization was already applied // As, otherwise, there's no point in creating a caching file, since there are no changes worth made to the file if (($jsContentBefore !== $jsContent) && (strpos($jsContent, '// #sourceMappingURL') !== false) && Misc::endsWith(trim($jsContent), '.map')) { $jsContent = OptimizeCommon::stripSourceMap($jsContent, 'js'); } $jsContentAfterAlterToCompare = $jsContent; // new possible values return array('content' => $jsContent , 'content_after_alter_to_compare' => $jsContentAfterAlterToCompare); } /** * @param $jsContent * @param $doJsMinify * * @return false|mixed|string|string[]|null */ public static function maybeAlterContentForInlineScriptTag($jsContent, $doJsMinify) { if (! trim($jsContent)) { // No Content! Return it as it, no point in doing extra checks return $jsContent; } if (mb_strlen($jsContent) > 500000) { // Bigger then ~500KB? Skip alteration for this inline SCRIPT return $jsContent; } $useCacheForInlineScript = true; if (mb_strlen($jsContent) < 40000) { // Smaller than ~40KB? Do not cache it $useCacheForInlineScript = false; } // For debugging purposes if (isset($_GET['wpacu_no_cache']) || (defined('WPACU_NO_CACHE') && WPACU_NO_CACHE === true)) { $useCacheForInlineScript = false; } if ($useCacheForInlineScript) { // 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 $jsContentBeforeHash = sha1( $jsContent ); $pathToInlineJsOptimizedItem = WP_CONTENT_DIR . self::getRelPathJsCacheDir() . '/item/inline/' . $jsContentBeforeHash . '.js'; // Check if the file exists before moving forward if ( is_file( $pathToInlineJsOptimizedItem ) ) { $cachedJsFileExpiresIn = OptimizeCommon::$cachedAssetFileExpiresIn; if ( filemtime( $pathToInlineJsOptimizedItem ) < ( time() - 1 * $cachedJsFileExpiresIn ) ) { // Has the caching period expired? Remove the file as a new one has to be generated @unlink( $pathToInlineJsOptimizedItem ); } else { // Not expired / Return its content from the cache in a faster way $inlineJsStorageItemJsonContent = FileSystem::fileGetContents( $pathToInlineJsOptimizedItem ); if ( $inlineJsStorageItemJsonContent !== '' ) { return $inlineJsStorageItemJsonContent; } } } } /* [START] Change JS Content */ if ($doJsMinify) { $jsContent = MinifyJs::applyMinification($jsContent); } if (Main::instance()->settings['google_fonts_remove']) { $jsContent = FontsGoogleRemove::stripReferencesFromJsCode($jsContent); } elseif (Main::instance()->settings['google_fonts_display']) { // Perhaps "display" parameter has to be applied to Google Font Links if they are active $jsContent = FontsGoogle::alterGoogleFontUrlFromJsContent($jsContent); } /* [END] Change JS Content */ if ( $useCacheForInlineScript && isset($pathToInlineJsOptimizedItem) ) { // Store the optimized content to the cached JS file which would be read quicker FileSystem::filePutContents( $pathToInlineJsOptimizedItem, $jsContent ); } return $jsContent; } /** * @param $htmlSource * * @return string */ public static function updateHtmlSourceOriginalToOptimizedJs($htmlSource) { $parseSiteUrlPath = (string)parse_url(site_url(), PHP_URL_PATH); $siteUrlNoProtocol = str_replace(array('http://', 'https://'), '//', site_url()); $jsOptimizeList = ObjectCache::wpacu_cache_get('wpacu_js_optimize_list') ?: array(); $allEnqueuedCleanSources = ObjectCache::wpacu_cache_get('wpacu_js_enqueued_srcs') ?: 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(); $cdnUrlForJs = isset($cdnUrls['js']) ? $cdnUrls['js'] : false; preg_match_all('#(]*src(|\s+)=(|\s+)[^>]*>)|(]*(as(\s+|)=(\s+|)(|"|\')script(|"|\'))[^>]*>)#Umi', $htmlSource, $matchesSourcesFromTags, PREG_SET_ORDER); $jsOptimizeListHardcoded = $scriptTagsToUpdate = array(); foreach ($matchesSourcesFromTags as $matches) { $scriptSourceTag = $matches[0]; if (strip_tags($scriptSourceTag) !== '') { // Hmm? Not a valid tag... Skip it... continue; } // Check if the JS has any 'data-wpacu-skip' attribute; if it does, do not alter it if (preg_match('#data-wpacu-skip([=>/ ])#i', $scriptSourceTag)) { continue; } $cleanScriptSrcFromTagArray = OptimizeCommon::getLocalCleanSourceFromTag($scriptSourceTag); // Skip external links, no point in carrying on if (! $cleanScriptSrcFromTagArray || ! is_array($cleanScriptSrcFromTagArray)) { continue; } $forAttr = 'src'; // Any preloads for the optimized script? // e.g. if (strpos($scriptSourceTag, ' $generatedHandle, 'src' => $cleanScriptSrcFromTag, 'ver' => md5($afterQuestionMark) ); $optimizeValues = self::maybeOptimizeIt($value); ObjectCache::wpacu_cache_set('wpacu_maybe_optimize_it_js_'.$generatedHandle, $optimizeValues); if (! empty($optimizeValues)) { $isHardcodedDetected = true; $jsOptimizeListHardcoded[] = $optimizeValues; } } if ( ! $isHardcodedDetected ) { $listToParse = $jsOptimizeList; } else { $listToParse = $jsOptimizeListHardcoded; } 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 '' if (strpos($cleanScriptSrcFromTag, $listValues[0]) === false) { continue; } // The contents of the CSS file has been changed and thus, we will replace the source path from 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 JS 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($cleanScriptSrcFromTag, $parseSiteUrlPath) === 0)) { $sourceUrlList[] = $cleanScriptSrcFromTag; } if ($parseSiteUrlPath && strpos($cleanScriptSrcFromTag, $parseSiteUrlPath) === 0 && strpos($cleanScriptSrcFromTag, $listValues[0]) !== false) { $sourceUrlList[] = str_replace('//', '/', $parseSiteUrlPath.'/'.$listValues[0]); } elseif ( $cleanScriptSrcFromTag === $listValues[0] ) { $sourceUrlList[] = $listValues[0]; } if ($cdnUrlForJs) { // Does it have a CDN? $sourceUrlList[] = OptimizeCommon::cdnToUrlFormat($cdnUrlForJs, 'rel') . $listValues[0]; } // Any rel tag? You never know // e.g. 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 no CDN is set, it will return site_url() as a prefix $optimizeUrl = OptimizeCommon::cdnToUrlFormat($cdnUrlForJs, 'raw') . $listValues[1]; // string if ($scriptSourceTag !== str_ireplace($sourceUrlList, $optimizeUrl, $scriptSourceTag)) { // 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.js') !== false && filesize($localPathOptimizedFile) < 10); if ($isEmptyOptimizedFile) { // Strip it as its content (after optimization, for instance) is empty; no point in having extra HTTP requests if ($forAttr === 'src') { $scriptTagsToUpdate[ $scriptSourceTag . '' ] = ''; } else { $scriptTagsToUpdate[ $scriptSourceTag ] = ''; // LINK (e.g. preload of a JS file) } // Note: As for September 3, 2020, the inline JS associated with the handle is no longer removed if the main JS file is empty // There could be cases when the main JS file is empty, but the inline JS tag associated with it has code that is needed } else { $newScriptSourceTag = self::updateOriginalToOptimizedTag( $scriptSourceTag, $sourceUrlList, $optimizeUrl, $forAttr ); $scriptTagsToUpdate[$scriptSourceTag] = $newScriptSourceTag; } break; } } } return strtr($htmlSource, $scriptTagsToUpdate); } /** * @param $scriptSourceTag string * @param $sourceUrlList array * @param $optimizeUrl string * @param string $forAttr ('src' (default), or 'href' if it's preloaded) * * @return string */ public static function updateOriginalToOptimizedTag($scriptSourceTag, $sourceUrlList, $optimizeUrl, $forAttr = 'src') { if (is_array($sourceUrlList) && ! empty($sourceUrlList)) { foreach ($sourceUrlList as $sourceUrl) { $newScriptSourceTag = str_replace($sourceUrl, $optimizeUrl, $scriptSourceTag); if ($newScriptSourceTag !== $scriptSourceTag) { break; } } } else { $newScriptSourceTag = str_replace( $sourceUrlList, $optimizeUrl, $scriptSourceTag ); } $tagToCheck = ($forAttr === 'src') ? 'script' : 'link'; $sourceUrlRel = is_array($sourceUrlList) ? OptimizeCommon::getSourceRelPath($sourceUrlList[0]) : OptimizeCommon::getSourceRelPath($sourceUrlList); $newScriptSourceTag = str_ireplace('<'.$tagToCheck.' ', '<'.$tagToCheck.' data-wpacu-script-rel-src-before="'.$sourceUrlRel.'" ', $newScriptSourceTag); $sourceValue = Misc::getValueFromTag($scriptSourceTag); // No space from the matching and ? should be there if ($sourceValue && ( strpos( $sourceValue, ' ' ) === false )) { if ( strpos( $sourceValue, '?' ) !== false ) { // Strip things like ?ver= list( , $toStrip ) = explode( '?', $sourceValue ); $toStrip = '?' . trim( $toStrip ); $newScriptSourceTag = str_replace( $toStrip, '', $newScriptSourceTag ); } if ( strpos( $sourceValue, '&ver' ) !== false ) { // Replace any .js&ver with .js $toStrip = strrchr($sourceValue, '&ver'); $newScriptSourceTag = str_replace( $toStrip, '', $newScriptSourceTag ); } } global $wp_version; $newScriptSourceTag = str_replace('.js&ver='.$wp_version, '.js', $newScriptSourceTag); $newScriptSourceTag = str_replace('.js&ver=', '.js', $newScriptSourceTag); return preg_replace('!\s+!', ' ', $newScriptSourceTag); // replace multiple spaces with only one space } /** * @param $htmlSource * * @return mixed|void */ public static function alterHtmlSource($htmlSource) { // There has to be at least one "preventAssetsSettings()) { /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_unload_ignore_deps_js'; Misc::scriptExecTimer($wpacuTimingName); /* [/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 $htmlSource = self::ignoreDependencyRuleAndKeepChildrenLoaded($htmlSource); /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ // Move any jQuery inline SCRIPT that is triggered before jQuery library is called through "jquery-core" handle if (Main::instance()->settings['move_inline_jquery_after_src_tag']) { /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_move_inline_jquery_after_src_tag'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */ $htmlSource = self::moveInlinejQueryAfterjQuerySrc($htmlSource); /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ } } $htmlSource = self::stripAnyReferencesForUnloadedScripts($htmlSource); /* * The JavaScript files only get cached if they are minified or are loaded like /?custom-js=version - /script.php?ver=1 etc. * #optimizing * STEP 2: Load optimize-able caching list and replace the original source URLs with the new cached ones */ // At least minify or cache dynamic loaded JS has to be enabled to proceed if (self::isWorthCheckingForOptimization()) { /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_original_to_optimized_js'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */ // 'wpacu_js_optimize_list' caching list is also checked; if it's empty, no optimization is made $htmlSource = self::updateHtmlSourceOriginalToOptimizedJs($htmlSource); /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ } if (! Main::instance()->preventAssetsSettings()) { /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_preload_js'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */ $preloads = Preloads::instance()->getPreloads(); if (isset($preloads['scripts']) && ! empty($preloads['scripts'])) { $htmlSource = Preloads::appendPreloadsForScriptsToHead($htmlSource); } /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ } /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_combine_js'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */ $proceedWithCombineOnThisPage = true; $isSingularPage = defined('WPACU_CURRENT_PAGE_ID') && WPACU_CURRENT_PAGE_ID > 0 && is_singular(); // If "Do not combine JS 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_js_optimize' refers to avoid the combination of JS files if ( (isset( $pageOptions['no_js_optimize'] ) && $pageOptions['no_js_optimize']) || (isset( $pageOptions['no_assets_settings'] ) && $pageOptions['no_assets_settings']) ) { $proceedWithCombineOnThisPage = false; } } if ($proceedWithCombineOnThisPage) { /* [wpacu_timing] */ // Note: Load timing is checked within the method /* [/wpacu_timing] */ $htmlSource = CombineJs::doCombine($htmlSource); if (defined('WPACU_REAPPLY_PRELOADING_FOR_COMBINED_JS') && WPACU_REAPPLY_PRELOADING_FOR_COMBINED_JS) { $htmlSource = Preloads::appendPreloadsForScriptsToHead($htmlSource); } } /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ if (self::isWorthCheckingForOptimization() && ! Main::instance()->preventAssetsSettings() && (MinifyJs::isMinifyJsEnabled() && in_array(Main::instance()->settings['minify_loaded_js_for'], array('inline', 'all')))) { /* [wpacu_timing] */ $wpacuTimingName = 'alter_html_source_for_minify_inline_script_tags'; Misc::scriptExecTimer($wpacuTimingName); /* [/wpacu_timing] */ $htmlSource = MinifyJs::minifyInlineScriptTags($htmlSource); /* [wpacu_timing] */ Misc::scriptExecTimer($wpacuTimingName, 'end'); /* [/wpacu_timing] */ } // Final cleanups $htmlSource = str_replace(Preloads::DEL_SCRIPTS_PRELOADS, '', $htmlSource); $htmlSource = preg_replace('#(\s+|)(data-wpacu-jquery-core-handle=1|data-wpacu-jquery-migrate-handle=1)(\s+|)#Umi', ' ', $htmlSource); $htmlSource = preg_replace('#(\s+|)data-wpacu-script-rel-src-before=(["\'])' . '(.*)' . '(\1)(\s+|)#Usmi', ' ', $htmlSource); $htmlSource = preg_replace('##Umi', '', $htmlSource); /* [wpacu_timing] */ Misc::scriptExecTimer('alter_html_source_for_optimize_js', 'end'); /* [/wpacu_timing] */ return $htmlSource; } /** * @return string */ public static function getRelPathJsCacheDir() { return OptimizeCommon::getRelPathPluginCacheDir().'js/'; // keep trailing slash at the end } /** * @param $scriptSrcs * @param $htmlSource * * @return array */ public static function getScriptTagsFromSrcs($scriptSrcs, $htmlSource) { $scriptTags = array(); foreach ($scriptSrcs as $scriptSrc) { $scriptSrc = str_replace('{site_url}', '', $scriptSrc); // Clean it up for the preg_quote() call if (strpos($scriptSrc, '.js?') !== false) { list($scriptSrc,) = explode('.js?', $scriptSrc); $scriptSrc .= '.js'; } preg_match_all('#]*src(|\s+)=(|\s+)[^>]*'. preg_quote($scriptSrc, '/'). '.*(>)(.*|)#Usmi', $htmlSource, $matchesFromSrc, PREG_SET_ORDER); if (isset($matchesFromSrc[0][0]) && strip_tags($matchesFromSrc[0][0]) === '') { $scriptTags[] = trim($matchesFromSrc[0][0]); } } return $scriptTags; } /** * @param $strFind * @param $strReplaceWith * @param $string * * @return mixed */ public static function strReplaceOnce($strFind, $strReplaceWith, $string) { if ( strpos($string, $strFind) === false ) { return $string; } $occurrence = strpos($string, $strFind); return substr_replace($string, $strReplaceWith, $occurrence, strlen($strFind)); } /** * @param $jsContent * @param $appendBefore * * @return string */ public static function maybeDoJsFixes($jsContent, $appendBefore) { // Relative URIs for CSS Paths // For code such as: // $(this).css("background", "url('../images/image-1.jpg')"); $jsContentPathReps = array( 'url("../' => 'url("'.$appendBefore.'../', "url('../" => "url('".$appendBefore.'../', 'url(../' => 'url('.$appendBefore.'../', 'url("./' => 'url("'.$appendBefore.'./', "url('./" => "url('".$appendBefore.'./', 'url(./' => 'url('.$appendBefore.'./' ); $jsContent = str_replace(array_keys($jsContentPathReps), array_values($jsContentPathReps), $jsContent); $jsContent = trim($jsContent); if (substr($jsContent, -1) !== ';') { $jsContent .= "\n" . ';'; // add semicolon as the last character } return $jsContent; } /** * e.g. if a script is unloaded, strip any LINK tag that preloads that script (e.g. added by other plugins) * * @param $htmlSource * * @return array|mixed|string|string[] */ public static function stripAnyReferencesForUnloadedScripts($htmlSource) { // Gather all SRCs of the unloaded scripts (if any) $unloadedScriptRelSrcs = array(); if ( isset( Main::instance()->allUnloadedAssets['scripts'] ) && ! empty( Main::instance()->allUnloadedAssets['scripts'] ) ) { foreach ( array_unique( Main::instance()->allUnloadedAssets['scripts'] ) as $scriptHandle ) { if ( ! (isset(Main::instance()->wpAllScripts['registered'][ $scriptHandle ]->src) && Main::instance()->wpAllScripts['registered'][ $scriptHandle ]->src) ) { continue; // does not have a "src" (e.g. inline JS) } $unloadedScriptRelSrcs[] = OptimizeCommon::getSourceRelPath( Main::instance()->wpAllScripts['registered'][ $scriptHandle ]->src ); } } if ( ! empty($unloadedScriptRelSrcs) ) { $htmlSource = OptimizeCommon::matchAndReplaceLinkTags($htmlSource, array('as' => 'script', 'unloaded_assets_rel_sources' => $unloadedScriptRelSrcs)); } return $htmlSource; } /** * @param $htmlSource * * @return false|mixed|string|void */ public static function moveInlinejQueryAfterjQuerySrc($htmlSource) { if (stripos($htmlSource, 'getElementsByTagName( 'script' ); if ($scriptTagsObj === null) { return $htmlSource; } // Does it have the "src" attribute? Skip it as it's not an inline SCRIPT tag $jQueryPatternsToMatch = array( 'jQuery', '\$(\s+|)\((\s+|)document(\s+|)\)(\s+|).(\s+|)ready(\s+|)\(' ); $jQueryRegExp = '#' . implode('|', $jQueryPatternsToMatch) . '#si'; $jQueryCoreDel = 'data-wpacu-jquery-core-handle='; $jQueryMigrateDel = 'data-wpacu-jquery-migrate-handle='; if (strpos($htmlSource, $jQueryMigrateDel) !== false) { $collectUntil = $jQueryMigrateDel; } elseif (strpos($htmlSource, $jQueryCoreDel) !== false) { $collectUntil = $jQueryCoreDel; } else { return $htmlSource; // No jQuery or jQuery Migrate? Just return the HTML source } $inlineBeforejQuerySrc = array(); foreach ($scriptTagsObj as $scriptTagObj) { $tagContents = $scriptTagObj->nodeValue; if ( strpos( Misc::getOuterHTML( $scriptTagObj ), $collectUntil) !== false) { break; } if ($tagContents !== '' && preg_match($jQueryRegExp, $tagContents)) { preg_match('#]*>'.preg_quote($tagContents, '/').'#si', $htmlSource, $matchesExact); $exactMatchTag = isset($matchesExact[0]) ? $matchesExact[0] : ''; // Replace the first match only in rare cases there are multiple SCRIPT tags with the same code if ($exactMatchTag && ($pos = strpos($htmlSource, $exactMatchTag)) !== false) { $inlineBeforejQuerySrc[] = $exactMatchTag; $htmlSource = substr_replace($htmlSource, '', $pos, strlen($exactMatchTag)); } } } preg_match('#]*>(.*?)#si', $htmlSource, $matches); if (! empty($inlineBeforejQuerySrc) && $collectUntil && isset($matches[0])) { $htmlSource = preg_replace('#]*>(.*?)#si', $matches[0]."\n".implode("\n", $inlineBeforejQuerySrc), $htmlSource); } return $htmlSource; } /** * @param string $returnType * 'list' - will return the list of plugins that have JS optimization enabled * 'if_enabled' - will stop when it finds the first one (any order) and return true * @return array|bool */ public static function isOptimizeJsEnabledByOtherParty($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' ); $jsOptimizeEnabledIn = array(); foreach ($pluginsToCheck as $plugin => $pluginTitle) { // "Autoptimize" check if ($plugin === 'autoptimize/autoptimize.php' && Misc::isPluginActive($plugin) && get_option('autoptimize_js')) { $jsOptimizeEnabledIn[] = $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')) { $wpRocketMinifyJs = get_rocket_option('minify_js'); $wpRocketMinifyConcatenateJs = get_rocket_option('minify_concatenate_js'); } else { $wpRocketSettings = get_option('wp_rocket_settings'); $wpRocketMinifyJs = isset($wpRocketSettings['minify_js']) ? $wpRocketSettings['minify_js'] : false; $wpRocketMinifyConcatenateJs = isset($wpRocketSettings['minify_concatenate_js']) ? $wpRocketSettings['minify_concatenate_js'] : false; } if ($wpRocketMinifyJs || $wpRocketMinifyConcatenateJs) { $jsOptimizeEnabledIn[] = $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['wpFastestCacheMinifyJs']) || isset($wpfcOptions['wpFastestCacheCombineJs'])) { $jsOptimizeEnabledIn[] = $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(); $w3tcEnableJs = (int)trim(Misc::extractBetween($w3tcConfigMaster, '"minify.js.enable":', ','), '" '); if ($w3tcEnableJs === 1) { $jsOptimizeEnabledIn[] = $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')) { if (@\SiteGround_Optimizer\Options\Options::is_enabled( 'siteground_optimizer_optimize_javascript')) { $jsOptimizeEnabledIn[] = $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 $jsOptimizeEnabledIn[] = $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['js_minify']) && $liteSpeedCacheConf['js_minify']) || (isset($liteSpeedCacheConf['js_combine']) && $liteSpeedCacheConf['js_combine']) ) { $jsOptimizeEnabledIn[] = $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-scripts', 1) ) { $jsOptimizeEnabledIn[] = $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-js'], $breezeAdvancedSettings['breeze-group-js']) && $breezeBasicSettings['breeze-minify-js'] && $breezeAdvancedSettings['breeze-group-js']) { $jsOptimizeEnabledIn[] = $pluginTitle; if ($returnType === 'if_enabled') { return true; } } } } if ($returnType === 'if_enabled') { return false; } return $jsOptimizeEnabledIn; } /** * @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 MinifyJs::isMinifyJsEnabled() || Main::instance()->settings['cache_dynamic_loaded_js'] || 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['scripts']) && ! empty($ignoreChild['scripts'])) { foreach (array_keys($ignoreChild['scripts']) as $scriptHandle) { if (isset(Main::instance()->wpAllScripts['registered'][$scriptHandle]->src, Main::instance()->ignoreChildren['scripts'][$scriptHandle.'_has_unload_rule']) && ($scriptSrc = Main::instance()->wpAllScripts['registered'][$scriptHandle]->src) && Main::instance()->ignoreChildren['scripts'][$scriptHandle.'_has_unload_rule']) { $toReplaceTagList = array(); // If the handle has any inline JavaScript associated with it (before or after the tag), make sure it's stripped as well if ($translationsContent = self::generateInlineAssocHtmlForHandle($scriptHandle, 'translations')) { $toReplaceTagList[] = $translationsContent; } if ($cDataContent = self::generateInlineAssocHtmlForHandle($scriptHandle, 'data')) { $toReplaceTagList[] = $cDataContent; } if ($beforeContent = self::generateInlineAssocHtmlForHandle($scriptHandle, 'before')) { $toReplaceTagList[] = $beforeContent; } $toReplaceTagList[] = self::getScriptTagFromHandle(array('data-wpacu-script-handle=[\'"]' . $scriptHandle . '[\'"]'), $htmlSource); if ($afterContent = self::generateInlineAssocHtmlForHandle($scriptHandle, 'after')) { $toReplaceTagList[] = $afterContent; } $htmlSource = str_replace($toReplaceTagList, '', $htmlSource); // Extra, in case the previous replacement didn't go through $listWithMatches = array(); $listWithMatches[] = 'data-wpacu-script-handle=[\'"]'.$scriptHandle.'[\'"]'; $listWithMatches[] = OptimizeCommon::getSourceRelPath($scriptSrc); $htmlSource = CleanUp::cleanScriptTagFromHtmlSource($listWithMatches, $htmlSource); } } } return $htmlSource; } /** * This would fetch the content of the SCRIPT tag (data, before & after) * * @param $scriptTagOrHandle * @param $wpacuRegisteredScripts * @param string $from * @param string $return ("value": JS Inline Content / "html": JS Inline Content surrounded by tags) * * @return array */ public static function getInlineAssociatedWithScriptHandle($scriptTagOrHandle, $wpacuRegisteredScripts, $from = 'tag', $return = 'value') { if ($from === 'tag') { preg_match_all('#data-wpacu-script-handle=([\'])' . '(.*)' . '(\1)#Usmi', $scriptTagOrHandle, $outputMatches); $scriptHandle = (isset($outputMatches[2][0]) && $outputMatches[2][0]) ? trim($outputMatches[2][0], '"\'') : ''; } else { // 'handle' $scriptHandle = $scriptTagOrHandle; } if ( $return === 'value' && $scriptHandle ) { $scriptExtraCdata = $scriptExtraBefore = $scriptExtraAfter = ''; if (isset($wpacuRegisteredScripts[$scriptHandle]->extra)) { $scriptExtraArray = $wpacuRegisteredScripts[ $scriptHandle ]->extra; if ( isset( $scriptExtraArray['data'] ) && $scriptExtraArray['data'] ) { $scriptExtraCdata .= $scriptExtraArray['data'] . "\n"; } if ( isset( $scriptExtraArray['before'] ) && ! empty( $scriptExtraArray['before'] ) ) { foreach ( $scriptExtraArray['before'] as $beforeData ) { if ( ! is_bool( $beforeData ) ) { $scriptExtraBefore .= $beforeData . "\n"; } } } if ( isset( $scriptExtraArray['after'] ) && ! empty( $scriptExtraArray['after'] ) ) { foreach ( $scriptExtraArray['after'] as $afterData ) { if ( ! is_bool( $afterData ) ) { $scriptExtraAfter .= $afterData . "\n"; } } } } $scriptTranslations = ''; if (method_exists('wp_scripts', 'print_translations')) { $scriptTranslations = wp_scripts()->print_translations( $scriptHandle, false ); } return array( 'translations' => trim($scriptTranslations), 'data' => trim($scriptExtraCdata), 'before' => trim($scriptExtraBefore), 'after' => trim($scriptExtraAfter) ); } if ( $return === 'html' && $scriptHandle ) { return array( 'translations' => self::generateInlineAssocHtmlForHandle($scriptHandle, 'translations'), 'data' => self::generateInlineAssocHtmlForHandle($scriptHandle, 'data'), 'before' => self::generateInlineAssocHtmlForHandle($scriptHandle, 'before'), 'after' => self::generateInlineAssocHtmlForHandle($scriptHandle, 'after') ); } return array('translations' => '', 'data' => '', 'before' => '', 'after' => ''); } /** * @param string $handle * @param string $position * @param string $inlineScriptContent * * @return string */ public static function generateInlineAssocHtmlForHandle($handle, $position, $inlineScriptContent = '') { global $wp_scripts; $output = ''; if ($position === 'translations') { $translations = false; if (method_exists('wp_scripts', 'print_translations')) { $translations = $wp_scripts->print_translations( $handle, false ); } if ( $translations ) { $output = sprintf( "\n%s\n\n", Misc::getScriptTypeAttribute(), esc_attr( $handle ), $translations ); } } if ( $position === 'data' ) { if ( ! $inlineScriptContent ) { $inlineScriptContent = $wp_scripts->get_data( $handle, 'data' ); if ( ! $inlineScriptContent ) { return ''; } } $output .= sprintf("\n", Misc::getScriptTypeAttribute(), esc_attr($handle)); // CDATA is not needed for HTML 5. if ( Misc::getScriptTypeAttribute() ) { $output .= "/* */\n"; } $output .= ''; } if ( $position === 'before' || $position === 'after' ) { if ( ! $inlineScriptContent ) { if (method_exists($wp_scripts, 'get_inline_script_data')) { // WordPress >= 6.3 $inlineScriptContent = $wp_scripts->get_inline_script_data( $handle, $position ); } else { // WordPress < 6.3 $inlineScriptContent = $wp_scripts->print_inline_script( $handle, $position, false ); } if ( ! $inlineScriptContent ) { $output = ''; } } if ( $inlineScriptContent ) { if (function_exists('wp_get_inline_script_tag')) { // WordPress >= 5.7.0 $id = "{$handle}-js-{$position}"; $output = wp_get_inline_script_tag( $inlineScriptContent, compact( 'id' ) ); } else { // WordPress < 5.7.0 $output = sprintf( "\n%s\n\n", Misc::getScriptTypeAttribute(), $handle, $position, $inlineScriptContent ); } } } return $output; } /** * @param $listWithPatterns * @param $htmlSource * * @return string */ public static function getScriptTagFromHandle($listWithPatterns, $htmlSource) { if (empty($listWithPatterns)) { return ''; } if (! is_array($listWithPatterns)) { $listWithPatterns = array($listWithPatterns); } preg_match_all( '#]*('.implode('|', $listWithPatterns).')[^>].*(>)#Usmi', $htmlSource, $matchesSourcesFromTags ); if (empty($matchesSourcesFromTags)) { return ''; } if (isset($matchesSourcesFromTags[0]) && ! empty($matchesSourcesFromTags[0])) { foreach ($matchesSourcesFromTags[0] as $matchesFromTag) { if (stripos($matchesFromTag, ' src=') !== false && strip_tags($matchesFromTag) === '') { return $matchesFromTag.''; } } } return ''; } }