481 lines
15 KiB
PHP
481 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* Assets Class
|
|
*
|
|
* Handles bundling of CSS and JavaScript files into single files
|
|
* for improved performance of static HTML pages.
|
|
*/
|
|
|
|
// Prevent direct access
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
class WP_To_HTML_Assets {
|
|
|
|
/**
|
|
* Single instance
|
|
*/
|
|
private static $instance = null;
|
|
|
|
/**
|
|
* Get singleton instance
|
|
*/
|
|
public static function get_instance() {
|
|
if (null === self::$instance) {
|
|
self::$instance = new self();
|
|
}
|
|
return self::$instance;
|
|
}
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
private function __construct() {
|
|
// Nothing to initialize
|
|
}
|
|
|
|
/**
|
|
* Bundle CSS and JS assets in the HTML
|
|
*
|
|
* @param string $html The HTML content
|
|
* @param int $post_id The post ID
|
|
* @param string $page_url The original page URL
|
|
* @return string Modified HTML with bundled assets
|
|
*/
|
|
public function bundle_assets($html, $post_id, $page_url) {
|
|
$settings = WP_To_HTML::get_settings();
|
|
|
|
// Bundle CSS if enabled
|
|
if (!empty($settings['bundle_css'])) {
|
|
$html = $this->bundle_css($html, $post_id, $page_url);
|
|
}
|
|
|
|
// Bundle JS if enabled
|
|
if (!empty($settings['bundle_js'])) {
|
|
$html = $this->bundle_js($html, $post_id, $page_url);
|
|
}
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Bundle all CSS files into a single file
|
|
*
|
|
* @param string $html The HTML content
|
|
* @param int $post_id The post ID
|
|
* @param string $page_url The original page URL
|
|
* @return string Modified HTML with bundled CSS
|
|
*/
|
|
private function bundle_css($html, $post_id, $page_url) {
|
|
// Extract all stylesheet links
|
|
$stylesheets = $this->extract_stylesheets($html);
|
|
|
|
if (empty($stylesheets)) {
|
|
return $html;
|
|
}
|
|
|
|
$bundled_css = '';
|
|
$base_url = $this->get_base_url($page_url);
|
|
|
|
// Download and combine all CSS files
|
|
foreach ($stylesheets as $stylesheet) {
|
|
$url = $stylesheet['href'];
|
|
$absolute_url = $this->make_absolute_url($url, $base_url);
|
|
|
|
$css_content = $this->download_asset($absolute_url);
|
|
|
|
if ($css_content !== false) {
|
|
// Rewrite URLs in CSS to be absolute
|
|
$css_content = $this->rewrite_css_urls($css_content, $absolute_url);
|
|
|
|
// Add source comment and content
|
|
$bundled_css .= "\n/* Source: {$absolute_url} */\n";
|
|
$bundled_css .= $css_content . "\n";
|
|
}
|
|
}
|
|
|
|
if (empty($bundled_css)) {
|
|
return $html;
|
|
}
|
|
|
|
// Generate a hash for the bundle filename
|
|
$bundle_hash = md5($bundled_css);
|
|
$bundle_filename = "bundle-{$post_id}-{$bundle_hash}.css";
|
|
|
|
// Save the bundled CSS file
|
|
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
|
|
if (!file_exists($assets_dir)) {
|
|
wp_mkdir_p($assets_dir);
|
|
}
|
|
|
|
$bundle_path = $assets_dir . $bundle_filename;
|
|
file_put_contents($bundle_path, $bundled_css);
|
|
|
|
// Get the URL for the bundled file
|
|
$bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename;
|
|
|
|
// Remove all original stylesheet links
|
|
foreach ($stylesheets as $stylesheet) {
|
|
$html = str_replace($stylesheet['full_tag'], '', $html);
|
|
}
|
|
|
|
// Add the bundled CSS link before </head>
|
|
$bundled_link = '<link rel="stylesheet" href="' . esc_url($bundle_url) . '" type="text/css" media="all" />';
|
|
$html = str_replace('</head>', $bundled_link . "\n</head>", $html);
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Bundle all JS files into a single file
|
|
*
|
|
* @param string $html The HTML content
|
|
* @param int $post_id The post ID
|
|
* @param string $page_url The original page URL
|
|
* @return string Modified HTML with bundled JS
|
|
*/
|
|
private function bundle_js($html, $post_id, $page_url) {
|
|
// Extract all script tags with src attributes
|
|
$scripts = $this->extract_scripts($html);
|
|
|
|
if (empty($scripts)) {
|
|
return $html;
|
|
}
|
|
|
|
$bundled_js = '';
|
|
$base_url = $this->get_base_url($page_url);
|
|
$has_defer = false;
|
|
$has_async = false;
|
|
|
|
// Download and combine all JS files
|
|
foreach ($scripts as $script) {
|
|
$url = $script['src'];
|
|
$absolute_url = $this->make_absolute_url($url, $base_url);
|
|
|
|
$js_content = $this->download_asset($absolute_url);
|
|
|
|
if ($js_content !== false) {
|
|
// Add source comment and content
|
|
$bundled_js .= "\n/* Source: {$absolute_url} */\n";
|
|
$bundled_js .= $js_content . ";\n";
|
|
}
|
|
|
|
// Track defer/async attributes
|
|
if (!empty($script['defer'])) {
|
|
$has_defer = true;
|
|
}
|
|
if (!empty($script['async'])) {
|
|
$has_async = true;
|
|
}
|
|
}
|
|
|
|
if (empty($bundled_js)) {
|
|
return $html;
|
|
}
|
|
|
|
// Generate a hash for the bundle filename
|
|
$bundle_hash = md5($bundled_js);
|
|
$bundle_filename = "bundle-{$post_id}-{$bundle_hash}.js";
|
|
|
|
// Save the bundled JS file
|
|
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
|
|
if (!file_exists($assets_dir)) {
|
|
wp_mkdir_p($assets_dir);
|
|
}
|
|
|
|
$bundle_path = $assets_dir . $bundle_filename;
|
|
file_put_contents($bundle_path, $bundled_js);
|
|
|
|
// Get the URL for the bundled file
|
|
$bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename;
|
|
|
|
// Remove all original script tags
|
|
foreach ($scripts as $script) {
|
|
$html = str_replace($script['full_tag'], '', $html);
|
|
}
|
|
|
|
// Build the bundled script tag with appropriate attributes
|
|
$attributes = '';
|
|
if ($has_defer) {
|
|
$attributes .= ' defer';
|
|
}
|
|
if ($has_async) {
|
|
$attributes .= ' async';
|
|
}
|
|
|
|
// Add the bundled JS script before </body>
|
|
$bundled_script = '<script src="' . esc_url($bundle_url) . '"' . $attributes . '></script>';
|
|
$html = str_replace('</body>', $bundled_script . "\n</body>", $html);
|
|
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Extract all stylesheet links from HTML
|
|
*
|
|
* @param string $html The HTML content
|
|
* @return array Array of stylesheet data
|
|
*/
|
|
private function extract_stylesheets($html) {
|
|
$stylesheets = array();
|
|
|
|
// Match link tags with rel="stylesheet"
|
|
$pattern = '/<link[^>]*rel=["\']stylesheet["\'][^>]*>/i';
|
|
preg_match_all($pattern, $html, $matches);
|
|
|
|
// Also match link tags where rel comes after href
|
|
$pattern2 = '/<link[^>]*href=["\'][^"\']+["\'][^>]*rel=["\']stylesheet["\'][^>]*>/i';
|
|
preg_match_all($pattern2, $html, $matches2);
|
|
|
|
$all_matches = array_unique(array_merge($matches[0], $matches2[0]));
|
|
|
|
foreach ($all_matches as $tag) {
|
|
// Extract href
|
|
if (preg_match('/href=["\']([^"\']+)["\']/i', $tag, $href_match)) {
|
|
$href = $href_match[1];
|
|
|
|
// Skip data URIs and inline styles
|
|
if (strpos($href, 'data:') === 0) {
|
|
continue;
|
|
}
|
|
|
|
$stylesheets[] = array(
|
|
'full_tag' => $tag,
|
|
'href' => $href,
|
|
);
|
|
}
|
|
}
|
|
|
|
return $stylesheets;
|
|
}
|
|
|
|
/**
|
|
* Extract all script tags with src from HTML
|
|
*
|
|
* @param string $html The HTML content
|
|
* @return array Array of script data
|
|
*/
|
|
private function extract_scripts($html) {
|
|
$scripts = array();
|
|
|
|
// Match script tags with src attribute
|
|
$pattern = '/<script[^>]*src=["\']([^"\']+)["\'][^>]*><\/script>/i';
|
|
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
|
|
|
|
foreach ($matches as $match) {
|
|
$tag = $match[0];
|
|
$src = $match[1];
|
|
|
|
// Skip data URIs
|
|
if (strpos($src, 'data:') === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Check for defer/async attributes
|
|
$defer = (stripos($tag, 'defer') !== false);
|
|
$async = (stripos($tag, 'async') !== false);
|
|
|
|
$scripts[] = array(
|
|
'full_tag' => $tag,
|
|
'src' => $src,
|
|
'defer' => $defer,
|
|
'async' => $async,
|
|
);
|
|
}
|
|
|
|
return $scripts;
|
|
}
|
|
|
|
/**
|
|
* Download an asset from a URL
|
|
*
|
|
* @param string $url The asset URL
|
|
* @return string|false The asset content or false on failure
|
|
*/
|
|
private function download_asset($url) {
|
|
$response = wp_remote_get($url, array(
|
|
'timeout' => 15,
|
|
'sslverify' => false,
|
|
));
|
|
|
|
if (is_wp_error($response)) {
|
|
return false;
|
|
}
|
|
|
|
$status_code = wp_remote_retrieve_response_code($response);
|
|
if ($status_code !== 200) {
|
|
return false;
|
|
}
|
|
|
|
return wp_remote_retrieve_body($response);
|
|
}
|
|
|
|
/**
|
|
* Rewrite relative URLs in CSS to be absolute
|
|
*
|
|
* @param string $css The CSS content
|
|
* @param string $css_url The original CSS file URL
|
|
* @return string CSS with absolute URLs
|
|
*/
|
|
private function rewrite_css_urls($css, $css_url) {
|
|
$base_url = dirname($css_url) . '/';
|
|
|
|
// Match url() patterns
|
|
$pattern = '/url\s*\(\s*["\']?(?!data:)(?!https?:\/\/)(?!\/\/)([^"\'\)]+)["\']?\s*\)/i';
|
|
|
|
$css = preg_replace_callback($pattern, function($matches) use ($base_url) {
|
|
$relative_url = $matches[1];
|
|
|
|
// Handle ../ paths
|
|
if (strpos($relative_url, '../') === 0) {
|
|
$absolute_url = $this->resolve_relative_path($base_url, $relative_url);
|
|
} elseif (strpos($relative_url, './') === 0) {
|
|
$absolute_url = $base_url . substr($relative_url, 2);
|
|
} else {
|
|
$absolute_url = $base_url . $relative_url;
|
|
}
|
|
|
|
return 'url("' . $absolute_url . '")';
|
|
}, $css);
|
|
|
|
// Handle @import statements
|
|
$import_pattern = '/@import\s+["\'](?!data:)(?!https?:\/\/)(?!\/\/)([^"\']+)["\']/i';
|
|
|
|
$css = preg_replace_callback($import_pattern, function($matches) use ($base_url) {
|
|
$relative_url = $matches[1];
|
|
$absolute_url = $this->resolve_relative_path($base_url, $relative_url);
|
|
return '@import "' . $absolute_url . '"';
|
|
}, $css);
|
|
|
|
return $css;
|
|
}
|
|
|
|
/**
|
|
* Resolve a relative path against a base URL
|
|
*
|
|
* @param string $base_url The base URL
|
|
* @param string $relative_path The relative path
|
|
* @return string The absolute URL
|
|
*/
|
|
private function resolve_relative_path($base_url, $relative_path) {
|
|
// Parse the base URL
|
|
$parsed = parse_url($base_url);
|
|
$scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : 'https://';
|
|
$host = isset($parsed['host']) ? $parsed['host'] : '';
|
|
$path = isset($parsed['path']) ? $parsed['path'] : '/';
|
|
|
|
// Remove filename from path if exists
|
|
if (substr($path, -1) !== '/') {
|
|
$path = dirname($path) . '/';
|
|
}
|
|
|
|
// Process the relative path
|
|
while (strpos($relative_path, '../') === 0) {
|
|
$relative_path = substr($relative_path, 3);
|
|
$path = dirname(rtrim($path, '/')) . '/';
|
|
}
|
|
|
|
// Remove leading ./
|
|
if (strpos($relative_path, './') === 0) {
|
|
$relative_path = substr($relative_path, 2);
|
|
}
|
|
|
|
return $scheme . $host . $path . $relative_path;
|
|
}
|
|
|
|
/**
|
|
* Make a URL absolute
|
|
*
|
|
* @param string $url The URL to make absolute
|
|
* @param string $base_url The base URL
|
|
* @return string The absolute URL
|
|
*/
|
|
private function make_absolute_url($url, $base_url) {
|
|
// Already absolute
|
|
if (preg_match('/^https?:\/\//i', $url)) {
|
|
return $url;
|
|
}
|
|
|
|
// Protocol-relative URL
|
|
if (strpos($url, '//') === 0) {
|
|
$parsed_base = parse_url($base_url);
|
|
$scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https';
|
|
return $scheme . ':' . $url;
|
|
}
|
|
|
|
// Absolute path (starts with /)
|
|
if (strpos($url, '/') === 0) {
|
|
$parsed_base = parse_url($base_url);
|
|
$scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https';
|
|
$host = isset($parsed_base['host']) ? $parsed_base['host'] : '';
|
|
return $scheme . '://' . $host . $url;
|
|
}
|
|
|
|
// Relative path
|
|
return $this->resolve_relative_path($base_url, $url);
|
|
}
|
|
|
|
/**
|
|
* Get the base URL from a page URL
|
|
*
|
|
* @param string $url The page URL
|
|
* @return string The base URL (without path)
|
|
*/
|
|
private function get_base_url($url) {
|
|
$parsed = parse_url($url);
|
|
$scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
|
|
$host = isset($parsed['host']) ? $parsed['host'] : '';
|
|
$path = isset($parsed['path']) ? $parsed['path'] : '/';
|
|
|
|
return $scheme . '://' . $host . $path;
|
|
}
|
|
|
|
/**
|
|
* Clear bundled assets from cache
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function clear_bundled_assets() {
|
|
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
|
|
|
|
if (!file_exists($assets_dir)) {
|
|
return true;
|
|
}
|
|
|
|
$files = glob($assets_dir . 'bundle-*');
|
|
|
|
foreach ($files as $file) {
|
|
if (is_file($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Delete bundled assets for a specific post
|
|
*
|
|
* @param int $post_id The post ID
|
|
* @return bool
|
|
*/
|
|
public function delete_post_assets($post_id) {
|
|
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
|
|
|
|
if (!file_exists($assets_dir)) {
|
|
return true;
|
|
}
|
|
|
|
$files = glob($assets_dir . "bundle-{$post_id}-*");
|
|
|
|
foreach ($files as $file) {
|
|
if (is_file($file)) {
|
|
unlink($file);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|