Files
WP-To-HTML/includes/class-generator.php
T
2026-03-18 14:21:32 -07:00

473 lines
14 KiB
PHP

<?php
/**
* HTML Generator Class
*
* Handles the generation of static HTML files from WordPress pages and posts.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Generator {
/**
* 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
}
/**
* Generate static HTML for a post/page
*
* @param int $post_id The post ID
* @return bool|WP_Error True on success, WP_Error on failure
*/
public function generate($post_id) {
$post = get_post($post_id);
if (!$post) {
return new WP_Error('invalid_post', __('Invalid post ID', 'wp-to-html'));
}
// Check if generation is enabled
$settings = WP_To_HTML::get_settings();
if (!$settings['enabled']) {
return new WP_Error('disabled', __('Static HTML generation is disabled', 'wp-to-html'));
}
// Check exclusions
$exclusion_reason = $this->should_exclude($post);
if ($exclusion_reason) {
return new WP_Error('excluded', $exclusion_reason);
}
// Get the permalink
$url = get_permalink($post_id);
if (!$url) {
return new WP_Error('no_permalink', __('Could not get permalink for post', 'wp-to-html'));
}
// Fetch the rendered page
$response = wp_remote_get($url, array(
'timeout' => 30,
'sslverify' => false,
'cookies' => array(), // No cookies to get the logged-out version
));
if (is_wp_error($response)) {
return $response;
}
$html = wp_remote_retrieve_body($response);
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return new WP_Error('bad_response', sprintf(__('Received HTTP %d response', 'wp-to-html'), $status_code));
}
if (empty($html)) {
return new WP_Error('empty_response', __('Received empty response', 'wp-to-html'));
}
// Get the cache path for this URL
$cache_path = $this->get_cache_path($url);
// Create directory if it doesn't exist
$cache_dir = dirname($cache_path);
if (!file_exists($cache_dir)) {
wp_mkdir_p($cache_dir);
}
// Bundle CSS and JS assets if enabled
$assets = WP_To_HTML_Assets::get_instance();
$html = $assets->bundle_assets($html, $post_id, $url);
// Add cache generation comment to HTML
$timestamp = current_time('mysql');
$cache_comment = "\n<!-- Generated by WP-to-HTML on {$timestamp} -->";
$html = preg_replace('/<\/html>/i', $cache_comment . "\n</html>", $html);
// Save the HTML file
$result = file_put_contents($cache_path, $html);
if ($result === false) {
return new WP_Error('write_failed', __('Failed to write cache file', 'wp-to-html'));
}
// Save metadata
$this->save_metadata($post_id, $url, $cache_path);
return true;
}
/**
* Check if a post should be excluded from static generation
*
* @param WP_Post $post The post object
* @return string|false Exclusion reason or false if not excluded
*/
public function should_exclude($post) {
// Check if post is published
if ($post->post_status !== 'publish') {
return __('Post is not published', 'wp-to-html');
}
// Check manual exclusion meta
$manual_exclude = get_post_meta($post->ID, '_wp_to_html_exclude', true);
if ($manual_exclude === '1' || $manual_exclude === 'yes') {
return __('Manually excluded via post meta', 'wp-to-html');
}
// Check if comments are open
if (comments_open($post->ID)) {
return __('Comments are enabled on this post', 'wp-to-html');
}
// Check for excluded shortcodes
$settings = WP_To_HTML::get_settings();
$excluded_shortcodes = array_map('trim', explode(',', $settings['excluded_shortcodes']));
foreach ($excluded_shortcodes as $shortcode) {
if (!empty($shortcode) && has_shortcode($post->post_content, $shortcode)) {
return sprintf(__('Contains excluded shortcode: %s', 'wp-to-html'), $shortcode);
}
}
// Check for excluded blocks (Gutenberg)
$excluded_block = $this->has_excluded_block($post, $settings);
if ($excluded_block) {
return sprintf(__('Contains excluded block: %s', 'wp-to-html'), $excluded_block);
}
// Check for WooCommerce
if ($this->is_woocommerce_page($post)) {
return __('WooCommerce page (dynamic content required)', 'wp-to-html');
}
return false;
}
/**
* Check if a post contains any excluded Gutenberg blocks
*
* @param WP_Post $post The post object
* @param array $settings Plugin settings
* @return string|false Block name if found, false otherwise
*/
private function has_excluded_block($post, $settings) {
// Get excluded blocks from settings
$excluded_blocks_setting = isset($settings['excluded_blocks']) ? $settings['excluded_blocks'] : '';
$excluded_blocks = array_filter(array_map('trim', explode(',', $excluded_blocks_setting)));
if (empty($excluded_blocks)) {
return false;
}
// Parse blocks from content
if (!function_exists('parse_blocks')) {
return false; // Gutenberg not available
}
$blocks = parse_blocks($post->post_content);
// Recursively check all blocks
return $this->find_excluded_block($blocks, $excluded_blocks);
}
/**
* Recursively search for excluded blocks
*
* @param array $blocks Array of parsed blocks
* @param array $excluded_blocks Array of excluded block names
* @return string|false Block name if found, false otherwise
*/
private function find_excluded_block($blocks, $excluded_blocks) {
foreach ($blocks as $block) {
$block_name = isset($block['blockName']) ? $block['blockName'] : '';
// Check if this block is excluded
foreach ($excluded_blocks as $excluded) {
if (!empty($excluded) && !empty($block_name)) {
// Support both full block names (core/form) and partial matches (wpforms)
if (stripos($block_name, $excluded) !== false) {
return $block_name;
}
}
}
// Check inner blocks recursively
if (!empty($block['innerBlocks'])) {
$found = $this->find_excluded_block($block['innerBlocks'], $excluded_blocks);
if ($found) {
return $found;
}
}
}
return false;
}
/**
* Check if a post is a WooCommerce page that should be excluded
*
* @param WP_Post $post The post object
* @return bool
*/
private function is_woocommerce_page($post) {
// Check if WooCommerce is active
if (!class_exists('WooCommerce')) {
return false;
}
// Check if it's a product
if ($post->post_type === 'product') {
return true;
}
// Check WooCommerce core pages
$woo_pages = array(
wc_get_page_id('cart'),
wc_get_page_id('checkout'),
wc_get_page_id('myaccount'),
wc_get_page_id('shop'),
);
if (in_array($post->ID, $woo_pages)) {
return true;
}
return false;
}
/**
* Get the cache file path for a URL
*
* @param string $url The page URL
* @return string The cache file path
*/
public function get_cache_path($url) {
$parsed = parse_url($url);
$path = isset($parsed['path']) ? $parsed['path'] : '/';
// Remove leading and trailing slashes
$path = trim($path, '/');
// Handle homepage
if (empty($path)) {
return WP_TO_HTML_CACHE_DIR . 'index.html';
}
// Create path with index.html
return WP_TO_HTML_CACHE_DIR . $path . '/index.html';
}
/**
* Save metadata for a cached file
*
* @param int $post_id The post ID
* @param string $url The original URL
* @param string $cache_path The cache file path
*/
private function save_metadata($post_id, $url, $cache_path) {
$metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/';
if (!file_exists($metadata_dir)) {
wp_mkdir_p($metadata_dir);
}
$metadata = array(
'post_id' => $post_id,
'url' => $url,
'cache_path' => $cache_path,
'generated_at' => current_time('mysql'),
'generated_timestamp' => time(),
);
$metadata_file = $metadata_dir . $post_id . '.json';
file_put_contents($metadata_file, json_encode($metadata, JSON_PRETTY_PRINT));
}
/**
* Delete cached file for a post
*
* @param int $post_id The post ID
* @return bool
*/
public function delete_cache($post_id) {
$post = get_post($post_id);
if (!$post) {
return false;
}
$url = get_permalink($post_id);
$cache_path = $this->get_cache_path($url);
// Delete the HTML file
if (file_exists($cache_path)) {
unlink($cache_path);
}
// Delete bundled assets for this post
$assets = WP_To_HTML_Assets::get_instance();
$assets->delete_post_assets($post_id);
// Delete metadata
$metadata_file = WP_TO_HTML_CACHE_DIR . '.metadata/' . $post_id . '.json';
if (file_exists($metadata_file)) {
unlink($metadata_file);
}
// Try to remove empty directories
$this->cleanup_empty_dirs(dirname($cache_path));
return true;
}
/**
* Clear entire cache
*
* @return bool
*/
public function clear_all_cache() {
if (!file_exists(WP_TO_HTML_CACHE_DIR)) {
return true;
}
$this->recursive_delete(WP_TO_HTML_CACHE_DIR, true);
// Recreate the cache directory structure
wp_mkdir_p(WP_TO_HTML_CACHE_DIR);
wp_mkdir_p(WP_TO_HTML_CACHE_DIR . '.metadata/');
wp_mkdir_p(WP_TO_HTML_CACHE_DIR . 'assets/');
// Add index.php for security
file_put_contents(WP_TO_HTML_CACHE_DIR . 'index.php', "<?php\n// Silence is golden.");
return true;
}
/**
* Recursively delete directory contents
*
* @param string $dir Directory path
* @param bool $keep_root Whether to keep the root directory
*/
private function recursive_delete($dir, $keep_root = false) {
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->recursive_delete($path);
} else {
unlink($path);
}
}
if (!$keep_root) {
rmdir($dir);
}
}
/**
* Cleanup empty directories
*
* @param string $dir Directory path
*/
private function cleanup_empty_dirs($dir) {
// Don't delete the main cache directory
if ($dir === WP_TO_HTML_CACHE_DIR || $dir === rtrim(WP_TO_HTML_CACHE_DIR, '/')) {
return;
}
if (is_dir($dir) && count(glob($dir . '/*')) === 0) {
rmdir($dir);
$this->cleanup_empty_dirs(dirname($dir));
}
}
/**
* Get stats about the cache
*
* @return array Cache statistics
*/
public function get_cache_stats() {
$stats = array(
'total_files' => 0,
'total_size' => 0,
'oldest_file' => null,
'newest_file' => null,
);
if (!file_exists(WP_TO_HTML_CACHE_DIR)) {
return $stats;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(WP_TO_HTML_CACHE_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'html') {
$stats['total_files']++;
$stats['total_size'] += $file->getSize();
$mtime = $file->getMTime();
if ($stats['oldest_file'] === null || $mtime < $stats['oldest_file']) {
$stats['oldest_file'] = $mtime;
}
if ($stats['newest_file'] === null || $mtime > $stats['newest_file']) {
$stats['newest_file'] = $mtime;
}
}
}
return $stats;
}
/**
* Check if a post has a cached version
*
* @param int $post_id The post ID
* @return bool
*/
public function has_cache($post_id) {
$post = get_post($post_id);
if (!$post) {
return false;
}
$url = get_permalink($post_id);
$cache_path = $this->get_cache_path($url);
return file_exists($cache_path);
}
}