473 lines
14 KiB
PHP
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);
|
|
}
|
|
}
|