First Commit

This commit is contained in:
2026-03-18 14:21:32 -07:00
parent 8712bbcc1a
commit 1cf5bbeeb0
12 changed files with 4185 additions and 0 deletions
+680
View File
@@ -0,0 +1,680 @@
<?php
/**
* Admin Class
*
* Handles the admin settings page and meta box for manual exclusion.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Admin {
/**
* 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() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
add_action('add_meta_boxes', array($this, 'add_meta_box'));
add_action('save_post', array($this, 'save_meta_box'), 10, 2);
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
// AJAX endpoint for regeneration status
add_action('wp_ajax_wp_to_html_get_status', array($this, 'ajax_get_status'));
}
/**
* Add admin menu
*/
public function add_admin_menu() {
add_options_page(
__('WP to HTML Settings', 'wp-to-html'),
__('WP to HTML', 'wp-to-html'),
'manage_options',
'wp-to-html',
array($this, 'render_settings_page')
);
}
/**
* Register settings
*/
public function register_settings() {
register_setting('wp_to_html_settings', 'wp_to_html_settings', array($this, 'sanitize_settings'));
// General Settings Section
add_settings_section(
'wp_to_html_general',
__('General Settings', 'wp-to-html'),
array($this, 'render_general_section'),
'wp-to-html'
);
add_settings_field(
'enabled',
__('Enable Static HTML Generation', 'wp-to-html'),
array($this, 'render_enabled_field'),
'wp-to-html',
'wp_to_html_general'
);
// Exclusion Settings Section
add_settings_section(
'wp_to_html_exclusions',
__('Exclusion Settings', 'wp-to-html'),
array($this, 'render_exclusions_section'),
'wp-to-html'
);
add_settings_field(
'excluded_shortcodes',
__('Excluded Shortcodes', 'wp-to-html'),
array($this, 'render_shortcodes_field'),
'wp-to-html',
'wp_to_html_exclusions'
);
add_settings_field(
'excluded_blocks',
__('Excluded Blocks', 'wp-to-html'),
array($this, 'render_blocks_field'),
'wp-to-html',
'wp_to_html_exclusions'
);
// Cron Settings Section
add_settings_section(
'wp_to_html_cron',
__('Scheduled Regeneration', 'wp-to-html'),
array($this, 'render_cron_section'),
'wp-to-html'
);
add_settings_field(
'cron_interval',
__('Regeneration Schedule', 'wp-to-html'),
array($this, 'render_cron_field'),
'wp-to-html',
'wp_to_html_cron'
);
// Performance Settings Section
add_settings_section(
'wp_to_html_performance',
__('Performance Settings', 'wp-to-html'),
array($this, 'render_performance_section'),
'wp-to-html'
);
add_settings_field(
'bundle_css',
__('Bundle CSS Files', 'wp-to-html'),
array($this, 'render_bundle_css_field'),
'wp-to-html',
'wp_to_html_performance'
);
add_settings_field(
'bundle_js',
__('Bundle JavaScript Files', 'wp-to-html'),
array($this, 'render_bundle_js_field'),
'wp-to-html',
'wp_to_html_performance'
);
// Cache Info Section
add_settings_section(
'wp_to_html_cache',
__('Cache Information', 'wp-to-html'),
array($this, 'render_cache_section'),
'wp-to-html'
);
}
/**
* Sanitize settings
*/
public function sanitize_settings($input) {
$sanitized = array();
$sanitized['enabled'] = isset($input['enabled']) ? (bool) $input['enabled'] : false;
$sanitized['excluded_shortcodes'] = isset($input['excluded_shortcodes'])
? sanitize_text_field($input['excluded_shortcodes'])
: '';
$sanitized['excluded_blocks'] = isset($input['excluded_blocks'])
? sanitize_text_field($input['excluded_blocks'])
: '';
// Sanitize cron interval
$valid_intervals = array('disabled', 'wp_to_html_hourly', 'wp_to_html_twicedaily', 'daily');
$sanitized['cron_interval'] = isset($input['cron_interval']) && in_array($input['cron_interval'], $valid_intervals)
? $input['cron_interval']
: 'wp_to_html_hourly';
// Sanitize bundling options
$sanitized['bundle_css'] = isset($input['bundle_css']) ? (bool) $input['bundle_css'] : false;
$sanitized['bundle_js'] = isset($input['bundle_js']) ? (bool) $input['bundle_js'] : false;
// Reschedule cron if interval changed
$current_settings = WP_To_HTML::get_settings();
if (!isset($current_settings['cron_interval']) || $current_settings['cron_interval'] !== $sanitized['cron_interval']) {
WP_To_HTML_Cron::reschedule_cron($sanitized['cron_interval']);
}
return $sanitized;
}
/**
* Enqueue admin assets
*/
public function enqueue_admin_assets($hook) {
if ($hook === 'settings_page_wp-to-html') {
wp_enqueue_style(
'wp-to-html-admin',
WP_TO_HTML_PLUGIN_URL . 'admin/css/admin.css',
array(),
WP_TO_HTML_VERSION
);
wp_enqueue_script(
'wp-to-html-admin',
WP_TO_HTML_PLUGIN_URL . 'admin/js/admin.js',
array('jquery'),
WP_TO_HTML_VERSION,
true
);
wp_localize_script('wp-to-html-admin', 'wpToHtml', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp_to_html_bulk'),
'statusNonce' => wp_create_nonce('wp_to_html_status'),
'strings' => array(
'processing' => __('Processing...', 'wp-to-html'),
'complete' => __('Complete!', 'wp-to-html'),
'error' => __('An error occurred', 'wp-to-html'),
'confirmClear' => __('Are you sure you want to clear all cached HTML files?', 'wp-to-html'),
'statusIdle' => __('No regeneration in progress', 'wp-to-html'),
'statusRunning' => __('Regeneration in progress...', 'wp-to-html'),
'statusPending' => __('Waiting to start...', 'wp-to-html'),
'statusComplete' => __('Regeneration complete', 'wp-to-html'),
'sourceCron' => __('(Scheduled Task)', 'wp-to-html'),
'sourceAdminBar' => __('(Admin Bar)', 'wp-to-html'),
'sourcePluginUpdate' => __('(Plugin Update)', 'wp-to-html'),
'sourceSettingsPage' => __('(Settings Page)', 'wp-to-html'),
),
));
}
}
/**
* Render settings page
*/
public function render_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
// Check if cache was just cleared
if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] === '1') {
add_settings_error('wp_to_html', 'cache_cleared', __('Cache cleared successfully.', 'wp-to-html'), 'success');
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<?php settings_errors('wp_to_html'); ?>
<div class="wp-to-html-admin-container">
<div class="wp-to-html-main">
<form action="options.php" method="post">
<?php
settings_fields('wp_to_html_settings');
do_settings_sections('wp-to-html');
submit_button(__('Save Settings', 'wp-to-html'));
?>
</form>
</div>
<div class="wp-to-html-sidebar">
<div class="wp-to-html-card">
<h3><?php _e('Bulk Actions', 'wp-to-html'); ?></h3>
<p><?php _e('Generate or clear cached HTML files in bulk.', 'wp-to-html'); ?></p>
<div class="wp-to-html-bulk-actions">
<button type="button" class="button button-primary" id="wp-to-html-convert-all">
<?php _e('Convert All Pages & Posts', 'wp-to-html'); ?>
</button>
<button type="button" class="button button-secondary" id="wp-to-html-clear-cache">
<?php _e('Clear All Cache', 'wp-to-html'); ?>
</button>
</div>
<div id="wp-to-html-progress" style="display: none;">
<div class="wp-to-html-progress-bar">
<div class="wp-to-html-progress-fill"></div>
</div>
<p class="wp-to-html-progress-text"></p>
</div>
<div id="wp-to-html-results" style="display: none;"></div>
</div>
<div class="wp-to-html-card" id="wp-to-html-status-card">
<h3><?php _e('Regeneration Status', 'wp-to-html'); ?></h3>
<div id="wp-to-html-live-status">
<?php
$status = WP_To_HTML_Cron::get_regeneration_status();
if ($status && in_array($status['status'], array('running', 'pending'))):
?>
<div class="status-running">
<span class="dashicons dashicons-update spin"></span>
<span class="status-text">
<?php echo $status['status'] === 'pending'
? __('Waiting to start...', 'wp-to-html')
: __('Regeneration in progress...', 'wp-to-html'); ?>
</span>
<?php if (isset($status['source'])): ?>
<span class="status-source">
<?php
$sources = array(
'cron' => __('(Scheduled Task)', 'wp-to-html'),
'admin_bar' => __('(Admin Bar)', 'wp-to-html'),
'plugin_update' => __('(Plugin Update)', 'wp-to-html'),
'settings_page' => __('(Settings Page)', 'wp-to-html'),
);
echo isset($sources[$status['source']]) ? $sources[$status['source']] : '';
?>
</span>
<?php endif; ?>
<?php if (isset($status['total']) && $status['total'] > 0): ?>
<div class="status-progress">
<?php printf(__('%d / %d pages processed', 'wp-to-html'), $status['processed'], $status['total']); ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($status && $status['status'] === 'complete'): ?>
<div class="status-complete">
<span class="dashicons dashicons-yes-alt"></span>
<span class="status-text"><?php _e('Last regeneration complete', 'wp-to-html'); ?></span>
<div class="status-details">
<?php printf(__('%d converted, %d skipped, %d errors', 'wp-to-html'),
$status['converted'], $status['skipped'], $status['errors']); ?>
</div>
</div>
<?php else: ?>
<div class="status-idle">
<span class="dashicons dashicons-clock"></span>
<span class="status-text"><?php _e('No regeneration in progress', 'wp-to-html'); ?></span>
</div>
<?php endif; ?>
</div>
</div>
<div class="wp-to-html-card">
<h3><?php _e('Server Configuration', 'wp-to-html'); ?></h3>
<p><?php _e('For Nginx servers, add this to your server configuration:', 'wp-to-html'); ?></p>
<pre class="wp-to-html-code"><?php echo esc_html($this->get_nginx_rules()); ?></pre>
</div>
</div>
</div>
</div>
<?php
}
/**
* Render general section description
*/
public function render_general_section() {
echo '<p>' . __('Configure the main settings for static HTML generation.', 'wp-to-html') . '</p>';
}
/**
* Render enabled field
*/
public function render_enabled_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[enabled]" value="1"
<?php checked($settings['enabled'], true); ?>>
<?php _e('Enable static HTML generation for pages and posts', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, pages and posts will be converted to static HTML files for faster loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render exclusions section description
*/
public function render_exclusions_section() {
echo '<p>' . __('Configure which pages should be excluded from static HTML generation.', 'wp-to-html') . '</p>';
}
/**
* Render shortcodes field
*/
public function render_shortcodes_field() {
$settings = WP_To_HTML::get_settings();
?>
<input type="text" name="wp_to_html_settings[excluded_shortcodes]"
value="<?php echo esc_attr($settings['excluded_shortcodes']); ?>"
class="large-text">
<p class="description">
<?php _e('Comma-separated list of shortcodes. Pages containing these shortcodes will not be converted to static HTML.', 'wp-to-html'); ?>
<br>
<?php _e('Example: contact-form-7, wpforms, gravityform', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render blocks field
*/
public function render_blocks_field() {
$settings = WP_To_HTML::get_settings();
?>
<input type="text" name="wp_to_html_settings[excluded_blocks]"
value="<?php echo esc_attr($settings['excluded_blocks']); ?>"
class="large-text">
<p class="description">
<?php _e('Comma-separated list of Gutenberg block names or partial names. Pages containing these blocks will not be converted to static HTML.', 'wp-to-html'); ?>
<br>
<?php _e('Example: wpforms, gravityforms, contact-form-7, ninja-forms', 'wp-to-html'); ?>
</p>
<p class="description">
<strong><?php _e('Automatic exclusions:', 'wp-to-html'); ?></strong>
<?php _e('Pages with comments enabled and WooCommerce pages (cart, checkout, my account, products) are automatically excluded.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render cron section description
*/
public function render_cron_section() {
echo '<p>' . __('Configure automatic regeneration of stale cached pages.', 'wp-to-html') . '</p>';
}
/**
* Render cron interval field
*/
public function render_cron_field() {
$settings = WP_To_HTML::get_settings();
$current_interval = isset($settings['cron_interval']) ? $settings['cron_interval'] : 'wp_to_html_hourly';
$intervals = array(
'disabled' => __('Disabled', 'wp-to-html'),
'wp_to_html_hourly' => __('Every Hour', 'wp-to-html'),
'wp_to_html_twicedaily' => __('Twice Daily', 'wp-to-html'),
'daily' => __('Daily', 'wp-to-html'),
);
?>
<select name="wp_to_html_settings[cron_interval]">
<?php foreach ($intervals as $value => $label): ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($current_interval, $value); ?>>
<?php echo esc_html($label); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
<?php _e('How often to check for and regenerate stale cached pages. Only pages that have been modified since their last cache will be regenerated.', 'wp-to-html'); ?>
</p>
<?php
// Show next scheduled run
$next_run = WP_To_HTML_Cron::get_next_run();
$last_run = WP_To_HTML_Cron::get_last_run();
if ($next_run): ?>
<p class="description">
<strong><?php _e('Next scheduled run:', 'wp-to-html'); ?></strong>
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)); ?>
</p>
<?php endif;
if ($last_run): ?>
<p class="description">
<strong><?php _e('Last run:', 'wp-to-html'); ?></strong>
<?php echo esc_html($last_run['time']); ?>
(<?php printf(__('%d pages regenerated', 'wp-to-html'), $last_run['regenerated']); ?>)
</p>
<?php endif;
}
/**
* Render performance section description
*/
public function render_performance_section() {
echo '<p>' . __('Configure performance optimizations for static HTML files.', 'wp-to-html') . '</p>';
}
/**
* Render bundle CSS field
*/
public function render_bundle_css_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[bundle_css]" value="1"
<?php checked(!empty($settings['bundle_css']), true); ?>>
<?php _e('Combine all CSS files into a single bundled file', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, all CSS stylesheets will be downloaded and combined into a single file for faster page loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render bundle JS field
*/
public function render_bundle_js_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[bundle_js]" value="1"
<?php checked(!empty($settings['bundle_js']), true); ?>>
<?php _e('Combine all JavaScript files into a single bundled file', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, all JavaScript files will be downloaded and combined into a single file for faster page loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render cache section
*/
public function render_cache_section() {
$generator = WP_To_HTML_Generator::get_instance();
$stats = $generator->get_cache_stats();
?>
<table class="wp-to-html-stats">
<tr>
<th><?php _e('Cache Directory:', 'wp-to-html'); ?></th>
<td><code><?php echo esc_html(WP_TO_HTML_CACHE_DIR); ?></code></td>
</tr>
<tr>
<th><?php _e('Cached Files:', 'wp-to-html'); ?></th>
<td><?php echo esc_html($stats['total_files']); ?></td>
</tr>
<tr>
<th><?php _e('Total Size:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(size_format($stats['total_size'])); ?></td>
</tr>
<?php if ($stats['oldest_file']): ?>
<tr>
<th><?php _e('Oldest Cache:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $stats['oldest_file'])); ?></td>
</tr>
<?php endif; ?>
<?php if ($stats['newest_file']): ?>
<tr>
<th><?php _e('Newest Cache:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $stats['newest_file'])); ?></td>
</tr>
<?php endif; ?>
</table>
<?php
}
/**
* Add meta box to post/page editor
*/
public function add_meta_box() {
$post_types = get_post_types(array('public' => true), 'names');
$excluded = array('attachment', 'product', 'product_variation');
$post_types = array_diff($post_types, $excluded);
foreach ($post_types as $post_type) {
add_meta_box(
'wp_to_html_exclude',
__('WP to HTML', 'wp-to-html'),
array($this, 'render_meta_box'),
$post_type,
'side',
'default'
);
}
}
/**
* Render meta box content
*/
public function render_meta_box($post) {
wp_nonce_field('wp_to_html_meta_box', 'wp_to_html_meta_box_nonce');
$exclude = get_post_meta($post->ID, '_wp_to_html_exclude', true);
$generator = WP_To_HTML_Generator::get_instance();
$has_cache = $generator->has_cache($post->ID);
$exclusion_reason = $generator->should_exclude($post);
?>
<div class="wp-to-html-meta-box">
<p>
<label>
<input type="checkbox" name="wp_to_html_exclude" value="1"
<?php checked($exclude, '1'); ?>>
<?php _e('Keep as dynamic PHP', 'wp-to-html'); ?>
</label>
</p>
<p class="description">
<?php _e('Check this to prevent this page from being converted to static HTML.', 'wp-to-html'); ?>
</p>
<hr>
<p class="wp-to-html-status">
<strong><?php _e('Status:', 'wp-to-html'); ?></strong><br>
<?php if ($exclusion_reason && $exclude !== '1'): ?>
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<?php echo esc_html($exclusion_reason); ?>
<?php elseif ($exclude === '1'): ?>
<span class="dashicons dashicons-lock" style="color: #72777c;"></span>
<?php _e('Manually excluded', 'wp-to-html'); ?>
<?php elseif ($has_cache): ?>
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
<?php _e('Cached as static HTML', 'wp-to-html'); ?>
<?php else: ?>
<span class="dashicons dashicons-clock" style="color: #72777c;"></span>
<?php _e('Not yet cached', 'wp-to-html'); ?>
<?php endif; ?>
</p>
</div>
<?php
}
/**
* Save meta box data
*/
public function save_meta_box($post_id, $post) {
// Verify nonce
if (!isset($_POST['wp_to_html_meta_box_nonce']) ||
!wp_verify_nonce($_POST['wp_to_html_meta_box_nonce'], 'wp_to_html_meta_box')) {
return;
}
// Check permissions
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Don't save for autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Save the exclusion setting
$exclude = isset($_POST['wp_to_html_exclude']) ? '1' : '';
update_post_meta($post_id, '_wp_to_html_exclude', $exclude);
}
/**
* Get Nginx configuration rules
*/
private function get_nginx_rules() {
$cache_path = str_replace(ABSPATH, '/', WP_TO_HTML_CACHE_DIR);
return "# WP-to-HTML Static Serving
location / {
# Skip for logged-in users
if (\$http_cookie ~* \"wordpress_logged_in\") {
set \$skip_cache 1;
}
# Skip for POST requests
if (\$request_method = POST) {
set \$skip_cache 1;
}
# Try to serve cached file
set \$cache_file \$document_root{$cache_path}\$uri/index.html;
if (-f \$cache_file) {
rewrite ^(.*)\$ {$cache_path}\$uri/index.html break;
}
}";
}
/**
* AJAX handler to get regeneration status
*/
public function ajax_get_status() {
check_ajax_referer('wp_to_html_status', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$status = WP_To_HTML_Cron::get_regeneration_status();
if (!$status) {
wp_send_json_success(array(
'status' => 'idle',
));
}
wp_send_json_success($status);
}
}
+480
View File
@@ -0,0 +1,480 @@
<?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;
}
}
+286
View File
@@ -0,0 +1,286 @@
<?php
/**
* Bulk Conversion Class
*
* Handles bulk conversion of pages and posts to static HTML.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Bulk {
/**
* Single instance
*/
private static $instance = null;
/**
* Batch size for processing
*/
const BATCH_SIZE = 10;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('wp_ajax_wp_to_html_bulk_convert', array($this, 'ajax_bulk_convert'));
add_action('wp_ajax_wp_to_html_clear_cache', array($this, 'ajax_clear_cache'));
add_action('wp_ajax_wp_to_html_get_posts_count', array($this, 'ajax_get_posts_count'));
}
/**
* AJAX handler for getting total posts count
*/
public function ajax_get_posts_count() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$count = $this->get_eligible_posts_count();
// Set initial status for bulk conversion
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => 'settings_page',
'started' => time(),
'total' => $count,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
wp_send_json_success(array(
'total' => $count,
));
}
/**
* AJAX handler for bulk conversion
*/
public function ajax_bulk_convert() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
$result = $this->process_batch($offset);
// Get current status to accumulate totals
$current_status = WP_To_HTML_Cron::get_regeneration_status();
$converted = isset($current_status['converted']) ? $current_status['converted'] : 0;
$skipped = isset($current_status['skipped']) ? $current_status['skipped'] : 0;
$errors = isset($current_status['errors']) ? $current_status['errors'] : 0;
$processed = isset($current_status['processed']) ? $current_status['processed'] : 0;
// Update status with accumulated progress
$new_status = array(
'source' => 'settings_page',
'started' => isset($current_status['started']) ? $current_status['started'] : time(),
'total' => $result['total'],
'processed' => $processed + $result['processed'],
'converted' => $converted + $result['converted'],
'skipped' => $skipped + $result['skipped'],
'errors' => $errors + $result['errors'],
);
if ($result['has_more']) {
$new_status['status'] = 'running';
} else {
$new_status['status'] = 'complete';
$new_status['completed'] = time();
}
WP_To_HTML_Cron::set_regeneration_status($new_status);
wp_send_json_success($result);
}
/**
* AJAX handler for clearing cache
*/
public function ajax_clear_cache() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
wp_send_json_success(array(
'message' => __('Cache cleared successfully', 'wp-to-html'),
));
}
/**
* Get count of eligible posts
*/
private function get_eligible_posts_count() {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
);
$query = new WP_Query($args);
return $query->post_count;
}
/**
* Process a batch of posts
*/
private function process_batch($offset) {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => self::BATCH_SIZE,
'offset' => $offset,
'orderby' => 'ID',
'order' => 'ASC',
);
$query = new WP_Query($args);
$generator = WP_To_HTML_Generator::get_instance();
$results = array(
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
'details' => array(),
'has_more' => false,
'next_offset' => $offset + self::BATCH_SIZE,
);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$post_title = get_the_title();
$results['processed']++;
$generation_result = $generator->generate($post_id);
if (is_wp_error($generation_result)) {
if ($generation_result->get_error_code() === 'excluded') {
$results['skipped']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'skipped',
'message' => $generation_result->get_error_message(),
);
} else {
$results['errors']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'error',
'message' => $generation_result->get_error_message(),
);
}
} else {
$results['converted']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'converted',
'message' => __('Successfully converted', 'wp-to-html'),
);
}
}
wp_reset_postdata();
}
// Check if there are more posts
$total = $this->get_eligible_posts_count();
$results['has_more'] = ($offset + self::BATCH_SIZE) < $total;
$results['total'] = $total;
return $results;
}
/**
* Get eligible post types
*/
private function get_eligible_post_types() {
$post_types = get_post_types(array('public' => true), 'names');
$excluded = array('attachment', 'product', 'product_variation');
return array_values(array_diff($post_types, $excluded));
}
/**
* Bulk convert all eligible posts (CLI method)
*/
public function convert_all() {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1,
);
$query = new WP_Query($args);
$generator = WP_To_HTML_Generator::get_instance();
$results = array(
'total' => $query->post_count,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$generation_result = $generator->generate($post_id);
if (is_wp_error($generation_result)) {
if ($generation_result->get_error_code() === 'excluded') {
$results['skipped']++;
} else {
$results['errors']++;
}
} else {
$results['converted']++;
}
}
wp_reset_postdata();
}
return $results;
}
}
+270
View File
@@ -0,0 +1,270 @@
<?php
/**
* Cron Class
*
* Handles scheduled cache regeneration for stale pages.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Cron {
/**
* Single instance
*/
private static $instance = null;
/**
* Cron hook name
*/
const CRON_HOOK = 'wp_to_html_scheduled_regeneration';
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Register the cron action
add_action(self::CRON_HOOK, array($this, 'run_scheduled_regeneration'));
// Add custom cron schedules
add_filter('cron_schedules', array($this, 'add_cron_schedules'));
}
/**
* Add custom cron schedules
*
* @param array $schedules Existing schedules
* @return array Modified schedules
*/
public function add_cron_schedules($schedules) {
$schedules['wp_to_html_hourly'] = array(
'interval' => HOUR_IN_SECONDS,
'display' => __('Every Hour (WP-to-HTML)', 'wp-to-html'),
);
$schedules['wp_to_html_twicedaily'] = array(
'interval' => 12 * HOUR_IN_SECONDS,
'display' => __('Twice Daily (WP-to-HTML)', 'wp-to-html'),
);
return $schedules;
}
/**
* Schedule the cron event
*/
public static function schedule_cron() {
$settings = WP_To_HTML::get_settings();
$interval = isset($settings['cron_interval']) ? $settings['cron_interval'] : 'wp_to_html_hourly';
// Don't schedule if disabled
if ($interval === 'disabled') {
self::unschedule_cron();
return;
}
// Only schedule if not already scheduled
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time(), $interval, self::CRON_HOOK);
}
}
/**
* Unschedule the cron event
*/
public static function unschedule_cron() {
$timestamp = wp_next_scheduled(self::CRON_HOOK);
if ($timestamp) {
wp_unschedule_event($timestamp, self::CRON_HOOK);
}
// Clear all events with this hook
wp_clear_scheduled_hook(self::CRON_HOOK);
}
/**
* Reschedule cron with new interval
*
* @param string $interval New interval
*/
public static function reschedule_cron($interval) {
self::unschedule_cron();
if ($interval !== 'disabled') {
wp_schedule_event(time(), $interval, self::CRON_HOOK);
}
}
/**
* Run the scheduled regeneration
*/
public function run_scheduled_regeneration() {
$settings = WP_To_HTML::get_settings();
// Check if plugin is enabled
if (!$settings['enabled']) {
return;
}
$generator = WP_To_HTML_Generator::get_instance();
$stale_posts = $this->get_stale_posts();
$total = count($stale_posts);
// Set initial status
self::set_regeneration_status(array(
'status' => 'running',
'source' => 'cron',
'started' => time(),
'total' => $total,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$converted = 0;
$errors = 0;
foreach ($stale_posts as $index => $post_id) {
$result = $generator->generate($post_id);
if (is_wp_error($result)) {
$errors++;
} else {
$converted++;
}
// Update status periodically (every 5 pages or at the end)
if (($index + 1) % 5 === 0 || ($index + 1) === $total) {
self::set_regeneration_status(array(
'status' => 'running',
'source' => 'cron',
'started' => time(),
'total' => $total,
'processed' => $index + 1,
'converted' => $converted,
'skipped' => 0,
'errors' => $errors,
));
}
}
// Mark complete
self::set_regeneration_status(array(
'status' => 'complete',
'source' => 'cron',
'completed' => time(),
'total' => $total,
'processed' => $total,
'converted' => $converted,
'skipped' => 0,
'errors' => $errors,
));
// Log the regeneration
update_option('wp_to_html_last_cron_run', array(
'time' => current_time('mysql'),
'timestamp' => time(),
'regenerated' => $converted,
));
}
/**
* Set regeneration status transient
*
* @param array $status Status data
*/
public static function set_regeneration_status($status) {
set_transient('wp_to_html_regeneration_status', $status, 5 * MINUTE_IN_SECONDS);
}
/**
* Get regeneration status
*
* @return array|false Status data or false if not set
*/
public static function get_regeneration_status() {
return get_transient('wp_to_html_regeneration_status');
}
/**
* Clear regeneration status
*/
public static function clear_regeneration_status() {
delete_transient('wp_to_html_regeneration_status');
}
/**
* Get posts that need regeneration
*
* @return array Array of post IDs
*/
private function get_stale_posts() {
$stale_posts = array();
$metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/';
if (!file_exists($metadata_dir)) {
return $stale_posts;
}
// Scan metadata directory for cached posts
$metadata_files = glob($metadata_dir . '*.json');
foreach ($metadata_files as $metadata_file) {
$metadata = json_decode(file_get_contents($metadata_file), true);
if (!$metadata || !isset($metadata['post_id'])) {
continue;
}
$post_id = $metadata['post_id'];
$post = get_post($post_id);
// Skip if post no longer exists
if (!$post) {
continue;
}
// Check if post was modified after cache was generated
$post_modified = strtotime($post->post_modified);
$cache_generated = isset($metadata['generated_timestamp']) ? $metadata['generated_timestamp'] : 0;
if ($post_modified > $cache_generated) {
$stale_posts[] = $post_id;
}
}
return $stale_posts;
}
/**
* Get the next scheduled run time
*
* @return int|false Timestamp or false if not scheduled
*/
public static function get_next_run() {
return wp_next_scheduled(self::CRON_HOOK);
}
/**
* Get the last cron run info
*
* @return array|false
*/
public static function get_last_run() {
return get_option('wp_to_html_last_cron_run', false);
}
}
+472
View File
@@ -0,0 +1,472 @@
<?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);
}
}
+785
View File
@@ -0,0 +1,785 @@
<?php
/**
* Hooks Class
*
* Handles WordPress hooks for automatic cache regeneration.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Hooks {
/**
* 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() {
$this->init_hooks();
}
/**
* Initialize all hooks
*/
private function init_hooks() {
// Post/page update hooks
add_action('save_post', array($this, 'on_save_post'), 20, 3);
add_action('delete_post', array($this, 'on_delete_post'), 10);
add_action('trash_post', array($this, 'on_delete_post'), 10);
add_action('transition_post_status', array($this, 'on_status_change'), 10, 3);
// Comment hooks
add_action('comment_post', array($this, 'on_comment_change'), 10, 2);
add_action('edit_comment', array($this, 'on_comment_edit'), 10);
add_action('delete_comment', array($this, 'on_comment_delete'), 10);
add_action('wp_set_comment_status', array($this, 'on_comment_status_change'), 10, 2);
// Global change hooks - clear entire cache
add_action('switch_theme', array($this, 'on_global_change'));
add_action('customize_save_after', array($this, 'on_global_change'));
add_action('update_option_sidebars_widgets', array($this, 'on_global_change'));
add_action('wp_update_nav_menu', array($this, 'on_global_change'));
// Plugin/theme update hooks - regenerate after other plugins update, skip for this plugin
add_action('upgrader_process_complete', array($this, 'on_plugin_update'), 10, 2);
// WooCommerce hooks (if active)
add_action('woocommerce_product_set_stock', array($this, 'on_woo_product_change'));
add_action('woocommerce_variation_set_stock', array($this, 'on_woo_product_change'));
// Admin bar indicator for cache status
add_action('admin_bar_menu', array($this, 'add_admin_bar_indicator'), 100);
add_action('wp_head', array($this, 'admin_bar_styles'));
add_action('admin_head', array($this, 'admin_bar_styles'));
add_action('wp_footer', array($this, 'admin_bar_scripts'));
add_action('admin_footer', array($this, 'admin_bar_scripts'));
// AJAX handlers for admin bar actions
add_action('wp_ajax_wp_to_html_adminbar_clear', array($this, 'ajax_adminbar_clear'));
add_action('wp_ajax_wp_to_html_adminbar_regenerate', array($this, 'ajax_adminbar_regenerate'));
add_action('wp_ajax_wp_to_html_adminbar_regenerate_page', array($this, 'ajax_adminbar_regenerate_page'));
add_action('wp_ajax_wp_to_html_adminbar_clear_regenerate', array($this, 'ajax_adminbar_clear_regenerate'));
}
/**
* Handle post save
*
* @param int $post_id Post ID
* @param WP_Post $post Post object
* @param bool $update Whether this is an update
*/
public function on_save_post($post_id, $post, $update) {
// Don't process revisions
if (wp_is_post_revision($post_id)) {
return;
}
// Don't process autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Only process pages and posts (and custom post types that are public)
$allowed_types = $this->get_allowed_post_types();
if (!in_array($post->post_type, $allowed_types)) {
return;
}
$generator = WP_To_HTML_Generator::get_instance();
// If the post is published, regenerate the cache
if ($post->post_status === 'publish') {
// Schedule regeneration to run after all other save operations
wp_schedule_single_event(time() + 5, 'wp_to_html_regenerate_post', array($post_id));
} else {
// Post is not published, delete the cache
$generator->delete_cache($post_id);
}
}
/**
* Handle post deletion
*
* @param int $post_id Post ID
*/
public function on_delete_post($post_id) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($post_id);
}
/**
* Handle post status transitions
*
* @param string $new_status New status
* @param string $old_status Old status
* @param WP_Post $post Post object
*/
public function on_status_change($new_status, $old_status, $post) {
$generator = WP_To_HTML_Generator::get_instance();
// If unpublishing, delete cache
if ($old_status === 'publish' && $new_status !== 'publish') {
$generator->delete_cache($post->ID);
}
// If publishing, generate cache
if ($new_status === 'publish' && $old_status !== 'publish') {
wp_schedule_single_event(time() + 5, 'wp_to_html_regenerate_post', array($post->ID));
}
}
/**
* Handle new comment
*
* @param int $comment_id Comment ID
* @param int|string $comment_approved Approval status
*/
public function on_comment_change($comment_id, $comment_approved) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment edit
*
* @param int $comment_id Comment ID
*/
public function on_comment_edit($comment_id) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment deletion
*
* @param int $comment_id Comment ID
*/
public function on_comment_delete($comment_id) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment status change
*
* @param int $comment_id Comment ID
* @param string $status New status
*/
public function on_comment_status_change($comment_id, $status) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle global changes that affect all pages
*/
public function on_global_change() {
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
}
/**
* Handle plugin/theme updates
*
* Regenerates cache when other plugins are updated, but skips when this plugin is updated
*
* @param WP_Upgrader $upgrader Upgrader instance
* @param array $options Update options including type and plugins
*/
public function on_plugin_update($upgrader, $options) {
// Only handle plugin updates
if (!isset($options['type']) || $options['type'] !== 'plugin') {
return;
}
// Get the list of plugins being updated
$plugins = array();
if (isset($options['plugins'])) {
$plugins = (array) $options['plugins'];
} elseif (isset($options['plugin'])) {
$plugins = array($options['plugin']);
}
// Check if our plugin is in the update list
$our_plugin = 'WP-to-HTML/wp-to-html.php';
$our_plugin_alt = plugin_basename(WP_TO_HTML_PLUGIN_DIR . 'wp-to-html.php');
foreach ($plugins as $plugin) {
if ($plugin === $our_plugin || $plugin === $our_plugin_alt) {
// This plugin is being updated - don't clear or regenerate
return;
}
}
// Other plugins are being updated - clear cache and regenerate
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
// Set pending status for plugin update regeneration
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'pending',
'source' => 'plugin_update',
'started' => time(),
'message' => __('Waiting for plugin update to complete...', 'wp-to-html'),
));
// Schedule regeneration to run after the update completes
wp_schedule_single_event(time() + 10, 'wp_to_html_regenerate_all', array('plugin_update'));
}
/**
* Handle WooCommerce product changes
*
* @param WC_Product $product Product object
*/
public function on_woo_product_change($product) {
// Products are excluded by default, but clear cache just in case
if (is_object($product) && method_exists($product, 'get_id')) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($product->get_id());
}
}
/**
* Get allowed post types for static generation
*
* @return array List of post type slugs
*/
private function get_allowed_post_types() {
$post_types = get_post_types(array(
'public' => true,
), 'names');
// Remove excluded types
$excluded = array('attachment', 'product', 'product_variation');
return array_diff($post_types, $excluded);
}
/**
* Add admin bar indicator for cache status
*
* @param WP_Admin_Bar $admin_bar Admin bar instance
*/
public function add_admin_bar_indicator($admin_bar) {
// Only show for users who can manage options
if (!current_user_can('manage_options')) {
return;
}
$settings = WP_To_HTML::get_settings();
$generator = WP_To_HTML_Generator::get_instance();
// Determine if we're on the frontend viewing a singular page
$is_frontend_singular = !is_admin() && is_singular();
if ($is_frontend_singular) {
// Frontend: Show page-specific status and regenerate option
$post_id = get_the_ID();
if (!$post_id) {
return;
}
$post = get_post($post_id);
// Determine cache status
$exclusion_reason = $generator->should_exclude($post);
$has_cache = $generator->has_cache($post_id);
if (!$settings['enabled']) {
$status = 'disabled';
$icon = '⏸';
$title = __('WP-to-HTML: Disabled', 'wp-to-html');
$color = '#72777c';
} elseif ($exclusion_reason) {
$status = 'excluded';
$icon = '⚠';
$title = sprintf(__('WP-to-HTML: Excluded - %s', 'wp-to-html'), $exclusion_reason);
$color = '#dba617';
} elseif ($has_cache) {
$status = 'cached';
$icon = '⚡';
$title = __('WP-to-HTML: Cached (Static HTML)', 'wp-to-html');
$color = '#00a32a';
} else {
$status = 'not-cached';
$icon = '○';
$title = __('WP-to-HTML: Not Cached Yet', 'wp-to-html');
$color = '#72777c';
}
$admin_bar->add_node(array(
'id' => 'wp-to-html-status',
'title' => '<span class="wp-to-html-indicator" style="color: ' . esc_attr($color) . ';" title="' . esc_attr($title) . '">' . $icon . '</span> <span class="wp-to-html-label">' . esc_html($title) . '</span>',
'href' => admin_url('options-general.php?page=wp-to-html'),
'meta' => array(
'class' => 'wp-to-html-admin-bar-item wp-to-html-status-' . $status,
),
));
// Add submenu items for frontend
$admin_bar->add_node(array(
'id' => 'wp-to-html-settings',
'parent' => 'wp-to-html-status',
'title' => __('⚙️ Settings', 'wp-to-html'),
'href' => admin_url('options-general.php?page=wp-to-html'),
));
// Only show regenerate if not excluded
if (!$exclusion_reason && $settings['enabled']) {
$admin_bar->add_node(array(
'id' => 'wp-to-html-regenerate-page',
'parent' => 'wp-to-html-status',
'title' => __('🔄 Regenerate This Page', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-regenerate-page',
'onclick' => 'return false;',
'data-post-id' => $post_id,
),
));
}
} else if (is_admin()) {
// Backend: Show global actions
$stats = $generator->get_cache_stats();
$icon = '⚡';
// Don't show "(0 cached)" - only show count if there are cached pages
if ($stats['total_files'] > 0) {
$title = sprintf(__('WP-to-HTML (%d cached)', 'wp-to-html'), $stats['total_files']);
} else {
$title = __('WP-to-HTML', 'wp-to-html');
}
$admin_bar->add_node(array(
'id' => 'wp-to-html-status',
'title' => '<span class="wp-to-html-indicator">' . $icon . '</span> <span class="wp-to-html-label">' . esc_html($title) . '</span>',
'href' => admin_url('options-general.php?page=wp-to-html'),
'meta' => array(
'class' => 'wp-to-html-admin-bar-item',
),
));
// Add submenu items for backend
$admin_bar->add_node(array(
'id' => 'wp-to-html-settings',
'parent' => 'wp-to-html-status',
'title' => __('⚙️ Settings', 'wp-to-html'),
'href' => admin_url('options-general.php?page=wp-to-html'),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-clear',
'parent' => 'wp-to-html-status',
'title' => __('🗑️ Clear All Cache', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-clear',
'onclick' => 'return false;',
),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-regenerate',
'parent' => 'wp-to-html-status',
'title' => __('🔄 Regenerate All', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-regenerate',
'onclick' => 'return false;',
),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-clear-regenerate',
'parent' => 'wp-to-html-status',
'title' => __('✨ Clear & Regenerate All', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-clear-regenerate',
'onclick' => 'return false;',
),
));
}
}
/**
* Add admin bar styles for the cache indicator
*/
public function admin_bar_styles() {
if (!is_admin_bar_showing() || !current_user_can('manage_options')) {
return;
}
?>
<style>
#wpadminbar .wp-to-html-indicator {
font-size: 16px;
margin-right: 4px;
}
#wpadminbar .wp-to-html-label {
font-size: 13px;
}
@media screen and (max-width: 782px) {
#wpadminbar .wp-to-html-label {
display: none;
}
}
#wpadminbar .wp-to-html-action-clear:hover,
#wpadminbar .wp-to-html-action-regenerate:hover {
cursor: pointer;
}
#wpadminbar .wp-to-html-processing .ab-item {
opacity: 0.6;
pointer-events: none;
}
</style>
<?php
}
/**
* Add admin bar scripts for AJAX actions
*/
public function admin_bar_scripts() {
if (!is_admin_bar_showing() || !current_user_can('manage_options')) {
return;
}
?>
<script>
(function() {
document.addEventListener('DOMContentLoaded', function() {
var clearBtn = document.querySelector('#wp-admin-bar-wp-to-html-clear .ab-item');
var regenBtn = document.querySelector('#wp-admin-bar-wp-to-html-regenerate .ab-item');
var regenPageBtn = document.querySelector('#wp-admin-bar-wp-to-html-regenerate-page .ab-item');
var nonce = '<?php echo wp_create_nonce('wp_to_html_adminbar'); ?>';
var ajaxUrl = '<?php echo admin_url('admin-ajax.php'); ?>';
if (clearBtn) {
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('Are you sure you want to clear all cached HTML files?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Clearing...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_clear&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
clearBtn.textContent = '<?php echo esc_js(__('✓ Cache Cleared!', 'wp-to-html')); ?>';
setTimeout(function() { location.reload(); }, 1000);
} else {
clearBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
clearBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
if (regenBtn) {
regenBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('This will regenerate all eligible pages. Continue?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_regenerate&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
regenBtn.textContent = '<?php echo esc_js(__('✓ Regenerated!', 'wp-to-html')); ?> (' + data.data.converted + ' pages)';
setTimeout(function() { location.reload(); }, 1500);
} else {
regenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
regenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
if (regenPageBtn) {
regenPageBtn.addEventListener('click', function(e) {
e.preventDefault();
var parent = this.parentElement;
var postId = parent.querySelector('[data-post-id]');
if (!postId) {
// Try to get post ID from the li element's data attribute
postId = document.querySelector('#wp-admin-bar-wp-to-html-regenerate-page');
}
// Extract post ID - it's stored in the meta
var postIdValue = postId ? postId.getAttribute('data-post-id') : null;
if (!postIdValue) {
// Fallback: get from current page URL or use a hidden input
postIdValue = '<?php echo is_singular() ? get_the_ID() : 0; ?>';
}
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_regenerate_page&nonce=' + nonce + '&post_id=' + postIdValue
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
regenPageBtn.textContent = '<?php echo esc_js(__('✓ Page Regenerated!', 'wp-to-html')); ?>';
setTimeout(function() { location.reload(); }, 1000);
} else {
regenPageBtn.textContent = data.data || '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
regenPageBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
// Clear & Regenerate All button
var clearRegenBtn = document.querySelector('#wp-admin-bar-wp-to-html-clear-regenerate .ab-item');
if (clearRegenBtn) {
clearRegenBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('This will clear all cache and regenerate all eligible pages. Continue?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Clearing and Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_clear_regenerate&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
clearRegenBtn.textContent = '<?php echo esc_js(__('✓ Done!', 'wp-to-html')); ?> (' + data.data.converted + ' pages)';
setTimeout(function() { location.reload(); }, 1500);
} else {
clearRegenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
clearRegenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
});
})();
</script>
<?php
}
/**
* AJAX handler for clearing cache from admin bar
*/
public function ajax_adminbar_clear() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
wp_send_json_success(array(
'message' => __('Cache cleared successfully', 'wp-to-html'),
));
}
/**
* AJAX handler for regenerating all from admin bar
*/
public function ajax_adminbar_regenerate() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
// Set initial status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => 'admin_bar',
'started' => time(),
'total' => 0,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
// Set complete status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'complete',
'source' => 'admin_bar',
'completed' => time(),
'total' => $results['total'],
'processed' => $results['total'],
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
wp_send_json_success(array(
'message' => __('Regeneration complete', 'wp-to-html'),
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
}
/**
* AJAX handler for regenerating a single page from admin bar
*/
public function ajax_adminbar_regenerate_page() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
if (!$post_id) {
wp_send_json_error(__('Invalid post ID', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$result = $generator->generate($post_id);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
}
wp_send_json_success(array(
'message' => __('Page regenerated successfully', 'wp-to-html'),
));
}
/**
* AJAX handler for clearing cache and regenerating all from admin bar
*/
public function ajax_adminbar_clear_regenerate() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
// First clear the cache
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
// Then regenerate all
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
wp_send_json_success(array(
'message' => __('Cache cleared and regeneration complete', 'wp-to-html'),
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
}
}
// Register the scheduled event handlers
add_action('wp_to_html_regenerate_post', function($post_id) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->generate($post_id);
});
// Register handler for regenerating all pages after plugin updates
add_action('wp_to_html_regenerate_all', function($source = 'plugin_update') {
// Set running status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => $source,
'started' => time(),
'total' => 0,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
// Set complete status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'complete',
'source' => $source,
'completed' => time(),
'total' => $results['total'],
'processed' => $results['total'],
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
});
+138
View File
@@ -0,0 +1,138 @@
<?php
/**
* Server Class
*
* Handles serving static HTML files as a PHP fallback when .htaccess rules don't work.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Server {
/**
* 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() {
// Hook early to serve cached content before WordPress runs
add_action('template_redirect', array($this, 'maybe_serve_cached'), 0);
}
/**
* Check if we should serve a cached file and do so if appropriate
*/
public function maybe_serve_cached() {
// Check if generation is enabled
$settings = WP_To_HTML::get_settings();
if (!$settings['enabled']) {
return;
}
// Don't serve cached content for logged-in users
if (is_user_logged_in()) {
return;
}
// Don't serve for POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return;
}
// Don't serve for requests with query strings
if (!empty($_SERVER['QUERY_STRING'])) {
return;
}
// Don't serve for admin pages
if (is_admin()) {
return;
}
// Don't serve for AJAX requests
if (wp_doing_ajax()) {
return;
}
// Don't serve for REST API requests
if (defined('REST_REQUEST') && REST_REQUEST) {
return;
}
// Get the current request URI
$request_uri = $_SERVER['REQUEST_URI'];
// Remove leading slash and get cache path
$path = trim(parse_url($request_uri, PHP_URL_PATH), '/');
if (empty($path)) {
$cache_file = WP_TO_HTML_CACHE_DIR . 'index.html';
} else {
$cache_file = WP_TO_HTML_CACHE_DIR . $path . '/index.html';
}
// Check if cached file exists
if (!file_exists($cache_file)) {
return;
}
// Serve the cached file
$this->serve_file($cache_file);
}
/**
* Serve a cached HTML file
*
* @param string $file_path Path to the cached file
*/
private function serve_file($file_path) {
// Set headers
header('Content-Type: text/html; charset=utf-8');
header('X-WP-To-HTML: served');
header('Cache-Control: public, max-age=3600');
// Get file modification time for caching headers
$mtime = filemtime($file_path);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
// Handle conditional requests
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$if_modified = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($if_modified >= $mtime) {
header('HTTP/1.1 304 Not Modified');
exit;
}
}
// Output the file
readfile($file_path);
exit;
}
/**
* Check if current request is being served from cache
*
* @return bool
*/
public static function is_serving_from_cache() {
return isset($_SERVER['HTTP_X_WP_TO_HTML']) ||
(function_exists('apache_request_headers') &&
isset(apache_request_headers()['X-WP-To-HTML']));
}
}