From 1cf5bbeeb0a5a8804f557b003ea73da467926148 Mon Sep 17 00:00:00 2001 From: Jeffrey Long Date: Wed, 18 Mar 2026 14:21:32 -0700 Subject: [PATCH] First Commit --- admin/css/admin.css | 267 ++++++++++++ admin/js/admin.js | 378 +++++++++++++++++ includes/class-admin.php | 680 ++++++++++++++++++++++++++++++ includes/class-assets.php | 480 +++++++++++++++++++++ includes/class-bulk.php | 286 +++++++++++++ includes/class-cron.php | 270 ++++++++++++ includes/class-generator.php | 472 +++++++++++++++++++++ includes/class-hooks.php | 785 +++++++++++++++++++++++++++++++++++ includes/class-server.php | 138 ++++++ readme.txt | 98 +++++ uninstall.php | 60 +++ wp-to-html.php | 271 ++++++++++++ 12 files changed, 4185 insertions(+) create mode 100644 admin/css/admin.css create mode 100644 admin/js/admin.js create mode 100644 includes/class-admin.php create mode 100644 includes/class-assets.php create mode 100644 includes/class-bulk.php create mode 100644 includes/class-cron.php create mode 100644 includes/class-generator.php create mode 100644 includes/class-hooks.php create mode 100644 includes/class-server.php create mode 100644 readme.txt create mode 100644 uninstall.php create mode 100644 wp-to-html.php diff --git a/admin/css/admin.css b/admin/css/admin.css new file mode 100644 index 0000000..98a72ee --- /dev/null +++ b/admin/css/admin.css @@ -0,0 +1,267 @@ +/** + * WP to HTML Admin Styles + */ + +.wp-to-html-admin-container { + display: flex; + gap: 20px; + margin-top: 20px; +} + +.wp-to-html-main { + flex: 1; + background: #fff; + padding: 20px; + border: 1px solid #c3c4c7; + border-radius: 4px; +} + +.wp-to-html-sidebar { + width: 350px; + flex-shrink: 0; +} + +.wp-to-html-card { + background: #fff; + padding: 20px; + border: 1px solid #c3c4c7; + border-radius: 4px; + margin-bottom: 20px; +} + +.wp-to-html-card h3 { + margin-top: 0; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.wp-to-html-bulk-actions { + display: flex; + flex-direction: column; + gap: 10px; +} + +.wp-to-html-bulk-actions .button { + text-align: center; +} + +.wp-to-html-progress-bar { + height: 20px; + background: #f0f0f1; + border-radius: 10px; + overflow: hidden; + margin: 15px 0; +} + +.wp-to-html-progress-fill { + height: 100%; + background: linear-gradient(90deg, #2271b1, #135e96); + width: 0%; + transition: width 0.3s ease; + border-radius: 10px; +} + +.wp-to-html-progress-text { + text-align: center; + color: #50575e; + font-size: 13px; + margin: 0; +} + +.wp-to-html-stats { + width: 100%; + border-collapse: collapse; +} + +.wp-to-html-stats th, +.wp-to-html-stats td { + padding: 8px 0; + text-align: left; + border-bottom: 1px solid #f0f0f1; +} + +.wp-to-html-stats th { + color: #50575e; + font-weight: 400; +} + +.wp-to-html-stats td { + color: #1d2327; +} + +.wp-to-html-stats code { + font-size: 12px; + word-break: break-all; +} + +.wp-to-html-code { + background: #f6f7f7; + padding: 15px; + border-radius: 4px; + font-size: 11px; + line-height: 1.5; + overflow-x: auto; + white-space: pre; + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +#wp-to-html-results { + margin-top: 15px; + padding: 15px; + background: #f6f7f7; + border-radius: 4px; + max-height: 300px; + overflow-y: auto; +} + +#wp-to-html-results .result-summary { + display: flex; + gap: 20px; + margin-bottom: 15px; + padding-bottom: 15px; + border-bottom: 1px solid #ddd; +} + +#wp-to-html-results .result-stat { + text-align: center; +} + +#wp-to-html-results .result-stat .number { + font-size: 24px; + font-weight: bold; + display: block; +} + +#wp-to-html-results .result-stat.converted .number { + color: #00a32a; +} + +#wp-to-html-results .result-stat.skipped .number { + color: #dba617; +} + +#wp-to-html-results .result-stat.errors .number { + color: #d63638; +} + +#wp-to-html-results .result-details { + font-size: 12px; +} + +#wp-to-html-results .result-item { + padding: 5px 0; + border-bottom: 1px solid #eee; + display: flex; + gap: 10px; + align-items: center; +} + +#wp-to-html-results .result-item:last-child { + border-bottom: none; +} + +#wp-to-html-results .result-item .dashicons { + font-size: 16px; + width: 16px; + height: 16px; +} + +#wp-to-html-results .result-item.converted .dashicons { + color: #00a32a; +} + +#wp-to-html-results .result-item.skipped .dashicons { + color: #dba617; +} + +#wp-to-html-results .result-item.error .dashicons { + color: #d63638; +} + +/* Meta box styles */ +.wp-to-html-meta-box hr { + margin: 12px 0; + border: 0; + border-top: 1px solid #ddd; +} + +.wp-to-html-meta-box .description { + color: #646970; + font-style: italic; + margin-top: 5px; +} + +.wp-to-html-status { + margin: 0; + line-height: 1.6; +} + +/* Responsive */ +@media screen and (max-width: 1200px) { + .wp-to-html-admin-container { + flex-direction: column; + } + + .wp-to-html-sidebar { + width: 100%; + } +} + +/* Regeneration Status Card */ +#wp-to-html-live-status { + min-height: 50px; +} + +#wp-to-html-live-status .dashicons { + font-size: 20px; + width: 20px; + height: 20px; + vertical-align: middle; + margin-right: 8px; +} + +#wp-to-html-live-status .status-text { + font-weight: 500; + vertical-align: middle; +} + +#wp-to-html-live-status .status-source { + color: #646970; + font-size: 12px; + margin-left: 5px; +} + +#wp-to-html-live-status .status-progress, +#wp-to-html-live-status .status-details { + margin-top: 8px; + padding-left: 28px; + color: #646970; + font-size: 13px; +} + +.status-running .dashicons { + color: #2271b1; +} + +.status-complete .dashicons { + color: #00a32a; +} + +.status-idle .dashicons { + color: #72777c; +} + +/* Spinning animation */ +.dashicons.spin { + animation: spin 1.5s linear infinite; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} \ No newline at end of file diff --git a/admin/js/admin.js b/admin/js/admin.js new file mode 100644 index 0000000..019231a --- /dev/null +++ b/admin/js/admin.js @@ -0,0 +1,378 @@ +/** + * WP to HTML Admin JavaScript + */ + +(function ($) { + 'use strict'; + + var WPToHTML = { + + // State + isProcessing: false, + totalPosts: 0, + processedPosts: 0, + results: { + converted: 0, + skipped: 0, + errors: 0, + details: [] + }, + statusPollInterval: null, + + /** + * Initialize + */ + init: function () { + this.bindEvents(); + this.startStatusPolling(); + }, + + /** + * Bind event handlers + */ + bindEvents: function () { + $('#wp-to-html-convert-all').on('click', $.proxy(this.startBulkConvert, this)); + $('#wp-to-html-clear-cache').on('click', $.proxy(this.clearCache, this)); + }, + + /** + * Start status polling + */ + startStatusPolling: function () { + var self = this; + + // Check status immediately + this.checkStatus(); + + // Poll every 3 seconds + this.statusPollInterval = setInterval(function () { + self.checkStatus(); + }, 3000); + }, + + /** + * Stop status polling + */ + stopStatusPolling: function () { + if (this.statusPollInterval) { + clearInterval(this.statusPollInterval); + this.statusPollInterval = null; + } + }, + + /** + * Check regeneration status + */ + checkStatus: function () { + var self = this; + + $.ajax({ + url: wpToHtml.ajaxUrl, + type: 'POST', + data: { + action: 'wp_to_html_get_status', + nonce: wpToHtml.statusNonce + }, + success: function (response) { + if (response.success) { + self.updateStatusCard(response.data); + } + } + }); + }, + + /** + * Update the status card UI + */ + updateStatusCard: function (status) { + var $container = $('#wp-to-html-live-status'); + if (!$container.length) return; + + var html = ''; + var strings = wpToHtml.strings; + + // Get source label + var sourceLabels = { + 'cron': strings.sourceCron, + 'admin_bar': strings.sourceAdminBar, + 'plugin_update': strings.sourcePluginUpdate, + 'settings_page': strings.sourceSettingsPage + }; + var sourceLabel = status.source ? (sourceLabels[status.source] || '') : ''; + + if (status.status === 'running' || status.status === 'pending') { + var statusText = status.status === 'pending' ? strings.statusPending : strings.statusRunning; + + html = '
'; + html += ''; + html += '' + statusText + ''; + if (sourceLabel) { + html += '' + sourceLabel + ''; + } + if (status.total && status.total > 0) { + html += '
' + status.processed + ' / ' + status.total + ' pages processed
'; + } + html += '
'; + } else if (status.status === 'complete') { + html = '
'; + html += ''; + html += '' + strings.statusComplete + ''; + html += '
' + status.converted + ' converted, ' + status.skipped + ' skipped, ' + status.errors + ' errors
'; + html += '
'; + } else { + // idle + html = '
'; + html += ''; + html += '' + strings.statusIdle + ''; + html += '
'; + } + + $container.html(html); + }, + + /** + * Start bulk conversion + */ + startBulkConvert: function (e) { + e.preventDefault(); + + if (this.isProcessing) { + return; + } + + this.isProcessing = true; + this.processedPosts = 0; + this.results = { + converted: 0, + skipped: 0, + errors: 0, + details: [] + }; + + // Disable buttons + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', true); + + // Show progress bar + $('#wp-to-html-progress').show(); + $('#wp-to-html-results').hide(); + this.updateProgress(0, wpToHtml.strings.processing); + + // Get total posts count first + this.getTotalPosts(); + }, + + /** + * Get total posts count + */ + getTotalPosts: function () { + var self = this; + + $.ajax({ + url: wpToHtml.ajaxUrl, + type: 'POST', + data: { + action: 'wp_to_html_get_posts_count', + nonce: wpToHtml.nonce + }, + success: function (response) { + if (response.success) { + self.totalPosts = response.data.total; + self.processBatch(0); + } else { + self.handleError(response.data); + } + }, + error: function () { + self.handleError(wpToHtml.strings.error); + } + }); + }, + + /** + * Process a batch of posts + */ + processBatch: function (offset) { + var self = this; + + $.ajax({ + url: wpToHtml.ajaxUrl, + type: 'POST', + data: { + action: 'wp_to_html_bulk_convert', + nonce: wpToHtml.nonce, + offset: offset + }, + success: function (response) { + if (response.success) { + // Update results + self.processedPosts += response.data.processed; + self.results.converted += response.data.converted; + self.results.skipped += response.data.skipped; + self.results.errors += response.data.errors; + self.results.details = self.results.details.concat(response.data.details); + + // Update progress + var percent = self.totalPosts > 0 + ? Math.round((self.processedPosts / self.totalPosts) * 100) + : 100; + self.updateProgress(percent, self.processedPosts + ' / ' + self.totalPosts); + + // Continue or finish + if (response.data.has_more) { + self.processBatch(response.data.next_offset); + } else { + self.finishBulkConvert(); + } + } else { + self.handleError(response.data); + } + }, + error: function () { + self.handleError(wpToHtml.strings.error); + } + }); + }, + + /** + * Finish bulk conversion + */ + finishBulkConvert: function () { + this.isProcessing = false; + + // Update progress to complete + this.updateProgress(100, wpToHtml.strings.complete); + + // Show results + this.showResults(); + + // Re-enable buttons + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false); + + // Refresh status immediately + this.checkStatus(); + }, + + /** + * Update progress bar + */ + updateProgress: function (percent, text) { + $('.wp-to-html-progress-fill').css('width', percent + '%'); + $('.wp-to-html-progress-text').text(text); + }, + + /** + * Show results + */ + showResults: function () { + var html = '
'; + html += '
' + this.results.converted + 'Converted
'; + html += '
' + this.results.skipped + 'Skipped
'; + html += '
' + this.results.errors + 'Errors
'; + html += '
'; + + if (this.results.details.length > 0) { + html += '
'; + + // Show up to 50 items + var items = this.results.details.slice(0, 50); + + for (var i = 0; i < items.length; i++) { + var item = items[i]; + var icon = 'yes-alt'; + var statusClass = item.status; + + if (item.status === 'skipped') { + icon = 'warning'; + } else if (item.status === 'error') { + icon = 'dismiss'; + } + + html += '
'; + html += ''; + html += '' + this.escapeHtml(item.title) + ''; + html += '' + this.escapeHtml(item.message) + ''; + html += '
'; + } + + if (this.results.details.length > 50) { + html += '

Showing first 50 results...

'; + } + + html += '
'; + } + + $('#wp-to-html-results').html(html).show(); + }, + + /** + * Clear cache + */ + clearCache: function (e) { + e.preventDefault(); + + if (this.isProcessing) { + return; + } + + if (!confirm(wpToHtml.strings.confirmClear)) { + return; + } + + this.isProcessing = true; + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', true); + + var self = this; + + $.ajax({ + url: wpToHtml.ajaxUrl, + type: 'POST', + data: { + action: 'wp_to_html_clear_cache', + nonce: wpToHtml.nonce + }, + success: function (response) { + self.isProcessing = false; + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false); + + if (response.success) { + alert(response.data.message); + location.reload(); + } else { + self.handleError(response.data); + } + }, + error: function () { + self.isProcessing = false; + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false); + self.handleError(wpToHtml.strings.error); + } + }); + }, + + /** + * Handle error + */ + handleError: function (message) { + this.isProcessing = false; + $('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false); + $('#wp-to-html-progress').hide(); + alert(message); + }, + + /** + * Escape HTML + */ + escapeHtml: function (text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + }; + + // Initialize on document ready + $(document).ready(function () { + WPToHTML.init(); + }); + +})(jQuery); + diff --git a/includes/class-admin.php b/includes/class-admin.php new file mode 100644 index 0000000..08c97e4 --- /dev/null +++ b/includes/class-admin.php @@ -0,0 +1,680 @@ + 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'); + } + + ?> +
+

+ + + +
+
+
+ +
+
+ +
+
+

+

+ +
+ + + +
+ + + + +
+ +
+

+
+ +
+ + + + + + + __('(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']] : ''; + ?> + + + 0): ?> +
+ +
+ +
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+

+

+
get_nginx_rules()); ?>
+
+
+
+
+ ' . __('Configure the main settings for static HTML generation.', 'wp-to-html') . '

'; + } + + /** + * Render enabled field + */ + public function render_enabled_field() { + $settings = WP_To_HTML::get_settings(); + ?> + +

+ +

+ ' . __('Configure which pages should be excluded from static HTML generation.', 'wp-to-html') . '

'; + } + + /** + * Render shortcodes field + */ + public function render_shortcodes_field() { + $settings = WP_To_HTML::get_settings(); + ?> + +

+ +
+ +

+ + +

+ +
+ +

+

+ + +

+ ' . __('Configure automatic regeneration of stale cached pages.', 'wp-to-html') . '

'; + } + + /** + * 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'), + ); + ?> + +

+ +

+ +

+ + +

+ +

+ + + () +

+ ' . __('Configure performance optimizations for static HTML files.', 'wp-to-html') . '

'; + } + + /** + * Render bundle CSS field + */ + public function render_bundle_css_field() { + $settings = WP_To_HTML::get_settings(); + ?> + +

+ +

+ + +

+ +

+ get_cache_stats(); + + ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 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); + + ?> +
+

+ +

+

+ +

+ +
+ +

+
+ + + + + + + + + + + + + +

+
+ 'idle', + )); + } + + wp_send_json_success($status); + } +} diff --git a/includes/class-assets.php b/includes/class-assets.php new file mode 100644 index 0000000..b473466 --- /dev/null +++ b/includes/class-assets.php @@ -0,0 +1,480 @@ +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 + $bundled_link = ''; + $html = str_replace('', $bundled_link . "\n", $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 + $bundled_script = ''; + $html = str_replace('', $bundled_script . "\n", $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 = '/]*rel=["\']stylesheet["\'][^>]*>/i'; + preg_match_all($pattern, $html, $matches); + + // Also match link tags where rel comes after href + $pattern2 = '/]*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 = '/]*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; + } +} diff --git a/includes/class-bulk.php b/includes/class-bulk.php new file mode 100644 index 0000000..aca210f --- /dev/null +++ b/includes/class-bulk.php @@ -0,0 +1,286 @@ +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; + } +} diff --git a/includes/class-cron.php b/includes/class-cron.php new file mode 100644 index 0000000..64507fa --- /dev/null +++ b/includes/class-cron.php @@ -0,0 +1,270 @@ + 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); + } +} diff --git a/includes/class-generator.php b/includes/class-generator.php new file mode 100644 index 0000000..9745085 --- /dev/null +++ b/includes/class-generator.php @@ -0,0 +1,472 @@ +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"; + $html = preg_replace('/<\/html>/i', $cache_comment . "\n", $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', "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); + } +} diff --git a/includes/class-hooks.php b/includes/class-hooks.php new file mode 100644 index 0000000..13de818 --- /dev/null +++ b/includes/class-hooks.php @@ -0,0 +1,785 @@ +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' => '' . $icon . ' ' . esc_html($title) . '', + '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' => '' . $icon . ' ' . esc_html($title) . '', + '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; + } + ?> + + + + 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'], + )); +}); diff --git a/includes/class-server.php b/includes/class-server.php new file mode 100644 index 0000000..cdc5b3a --- /dev/null +++ b/includes/class-server.php @@ -0,0 +1,138 @@ +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'])); + } +} diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..c78dafc --- /dev/null +++ b/readme.txt @@ -0,0 +1,98 @@ +=== WP to HTML === +Contributors: yourname +Tags: static html, performance, speed, cache, optimization +Requires at least: 5.0 +Tested up to: 6.4 +Requires PHP: 7.2 +Stable tag: 0.2.4 +License: GPLv2 or later +License URI: https://www.gnu.org/licenses/gpl-2.0.html + +Converts WordPress pages and posts to static HTML files for dramatically faster page loading. + +== Description == + +WP to HTML is a performance optimization plugin that converts your WordPress pages and posts into static HTML files. When visitors access your site, they receive pre-rendered HTML instead of waiting for PHP to process each request, resulting in significantly faster page load times. + +**Key Features:** + +* **Automatic HTML Generation** - Pages and posts are automatically converted to static HTML when published or updated +* **Scheduled Regeneration** - Cron job automatically regenerates stale cached pages +* **Smart Exclusion Rules** - Automatically excludes pages with forms, comments, or WooCommerce functionality +* **Manual Exclusion** - Checkbox in the editor to keep specific pages dynamic +* **Bulk Conversion** - Convert all existing pages and posts with a single click +* **Admin Bar Actions** - Clear cache or regenerate all pages directly from the admin bar +* **Clean Uninstall** - Removes all cache files and settings when plugin is deleted +* **Logged-in User Bypass** - Administrators always see the dynamic version for editing +* **WooCommerce Compatible** - Automatically keeps cart, checkout, and product pages dynamic +* **Apache & Nginx Support** - Works with both major web servers + +**How It Works:** + +1. When a page or post is published, the plugin fetches the fully-rendered HTML +2. The HTML is saved to the cache directory, mirroring your site's URL structure +3. Future visitors receive the cached HTML file directly from the server +4. When content is updated, the cache is automatically regenerated + +**Automatic Exclusions:** + +The plugin intelligently excludes pages that require dynamic functionality: + +* Pages with comments enabled +* Pages containing form shortcodes (Contact Form 7, WPForms, Gravity Forms, etc.) +* WooCommerce pages (cart, checkout, my account, products) +* Any page you manually mark as dynamic + +== Installation == + +1. Upload the `wp-to-html` directory to `/wp-content/plugins/` +2. Activate the plugin through the 'Plugins' menu in WordPress +3. Go to Settings → WP to HTML to configure the plugin +4. Click "Convert All Pages & Posts" to generate static HTML for existing content + +== Frequently Asked Questions == + += Will this work with my theme? = + +Yes! The plugin fetches the fully-rendered page including your theme, so it works with any WordPress theme. + += What about pages with dynamic content? = + +Pages with dynamic content (forms, comments, WooCommerce features) are automatically excluded. You can also manually exclude any page using the checkbox in the editor. + += How do I know if a page is being served from cache? = + +Cached pages include an HTML comment at the bottom with the generation timestamp. You can also check the response headers for `X-WP-To-HTML: served`. + += Does this work with Nginx? = + +Yes, but you'll need to add the provided configuration rules to your Nginx server config. Apache servers work automatically via .htaccess. + += What happens if I deactivate the plugin? = + +The cached files remain but are no longer served. Your site will function normally using dynamic PHP. + += What happens when I delete the plugin? = + +When you delete the plugin through WordPress, all cache files and settings are automatically removed. + +== Changelog == + += 0.2.0 = +* Added scheduled regeneration via WordPress cron (hourly/twice daily/daily options) +* Added admin bar submenu with "Clear All Cache" and "Regenerate All" options +* Added uninstall.php to clean up cache files and settings on plugin deletion +* Added configurable cron interval in settings + += 0.1.2 = +* Added support for Custom Post Types +* Added settings link on plugin listing page +* Improved block exclusion detection + += 1.0.0 = +* Initial release +* Static HTML generation for pages and posts +* Automatic and manual exclusion options +* Bulk conversion tool +* WooCommerce compatibility +* Apache and Nginx support diff --git a/uninstall.php b/uninstall.php new file mode 100644 index 0000000..af21e35 --- /dev/null +++ b/uninstall.php @@ -0,0 +1,60 @@ +load_dependencies(); + $this->init_hooks(); + } + + /** + * Load required class files + */ + private function load_dependencies() { + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-generator.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-server.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-hooks.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-admin.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-bulk.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-cron.php'; + require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-assets.php'; + } + + /** + * Initialize WordPress hooks + */ + private function init_hooks() { + // Initialize components + add_action('init', array($this, 'init_components')); + + // Register activation/deactivation hooks + register_activation_hook(__FILE__, array($this, 'activate')); + register_deactivation_hook(__FILE__, array($this, 'deactivate')); + } + + /** + * Initialize plugin components + */ + public function init_components() { + WP_To_HTML_Generator::get_instance(); + WP_To_HTML_Server::get_instance(); + WP_To_HTML_Hooks::get_instance(); + WP_To_HTML_Cron::get_instance(); + + if (is_admin()) { + WP_To_HTML_Admin::get_instance(); + WP_To_HTML_Bulk::get_instance(); + } + } + + /** + * Plugin activation + */ + public function activate() { + // Create cache directory + if (!file_exists(WP_TO_HTML_CACHE_DIR)) { + wp_mkdir_p(WP_TO_HTML_CACHE_DIR); + } + + // Create metadata directory + $metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/'; + if (!file_exists($metadata_dir)) { + wp_mkdir_p($metadata_dir); + } + + // Add index.php to cache directory for security + $index_content = " true, + 'excluded_shortcodes' => 'contact-form-7,wpforms,gravityform,ninja_forms,formidable,wpcf7', + 'excluded_blocks' => 'wpforms,gravityforms,contact-form-7,ninja-forms,formidable,forminator,ws-form,fluentform,everest-forms', + 'bundle_css' => true, + 'bundle_js' => true, + ); + + if (!get_option('wp_to_html_settings')) { + add_option('wp_to_html_settings', $default_options); + } + + // Add htaccess rules + $this->add_htaccess_rules(); + + // Schedule cron event + WP_To_HTML_Cron::schedule_cron(); + + // Flush rewrite rules + flush_rewrite_rules(); + } + + /** + * Plugin deactivation + */ + public function deactivate() { + // Remove htaccess rules + $this->remove_htaccess_rules(); + + // Unschedule cron event + WP_To_HTML_Cron::unschedule_cron(); + + // Flush rewrite rules + flush_rewrite_rules(); + + // Note: We don't delete the cache directory on deactivation + // to preserve cached files in case of accidental deactivation + } + + /** + * Add htaccess rules for serving static HTML + */ + private function add_htaccess_rules() { + $htaccess_file = ABSPATH . '.htaccess'; + + if (!is_writable($htaccess_file)) { + return false; + } + + $htaccess_content = file_get_contents($htaccess_file); + + // Check if rules already exist + if (strpos($htaccess_content, '# BEGIN WP-to-HTML') !== false) { + return true; + } + + $rules = $this->get_htaccess_rules(); + + // Add rules before WordPress rules + $wp_marker = '# BEGIN WordPress'; + if (strpos($htaccess_content, $wp_marker) !== false) { + $htaccess_content = str_replace($wp_marker, $rules . "\n\n" . $wp_marker, $htaccess_content); + } else { + $htaccess_content = $rules . "\n\n" . $htaccess_content; + } + + return file_put_contents($htaccess_file, $htaccess_content); + } + + /** + * Remove htaccess rules + */ + private function remove_htaccess_rules() { + $htaccess_file = ABSPATH . '.htaccess'; + + if (!is_writable($htaccess_file)) { + return false; + } + + $htaccess_content = file_get_contents($htaccess_file); + + // Remove our rules block + $pattern = '/# BEGIN WP-to-HTML.*?# END WP-to-HTML\s*/s'; + $htaccess_content = preg_replace($pattern, '', $htaccess_content); + + return file_put_contents($htaccess_file, $htaccess_content); + } + + /** + * Get htaccess rules + */ + public function get_htaccess_rules() { + $cache_path = str_replace(ABSPATH, '/', WP_TO_HTML_CACHE_DIR); + + $rules = "# BEGIN WP-to-HTML\n"; + $rules .= "\n"; + $rules .= "RewriteEngine On\n"; + $rules .= "RewriteBase /\n\n"; + + // Skip for logged-in users (WordPress login cookies) + $rules .= "# Skip for logged-in users\n"; + $rules .= "RewriteCond %{HTTP_COOKIE} !wordpress_logged_in [NC]\n\n"; + + // Skip for POST requests + $rules .= "# Skip for POST requests\n"; + $rules .= "RewriteCond %{REQUEST_METHOD} !POST\n\n"; + + // Skip for query strings + $rules .= "# Skip for requests with query strings\n"; + $rules .= "RewriteCond %{QUERY_STRING} ^$\n\n"; + + // Check if static file exists and serve it + $rules .= "# Check if cached HTML exists and serve it\n"; + $rules .= "RewriteCond %{DOCUMENT_ROOT}" . $cache_path . "%{REQUEST_URI}index.html -f\n"; + $rules .= "RewriteRule ^(.*)$ " . $cache_path . "%{REQUEST_URI}index.html [L]\n"; + + $rules .= "\n"; + $rules .= "# END WP-to-HTML"; + + return $rules; + } + + /** + * Get plugin settings + */ + public static function get_settings() { + $defaults = array( + 'enabled' => true, + 'excluded_shortcodes' => 'contact-form-7,wpforms,gravityform,ninja_forms,formidable,wpcf7', + 'excluded_blocks' => 'wpforms,gravityforms,contact-form-7,ninja-forms,formidable,forminator,ws-form,fluentform,everest-forms', + 'cron_interval' => 'wp_to_html_hourly', + 'bundle_css' => true, + 'bundle_js' => true, + ); + + $settings = get_option('wp_to_html_settings', $defaults); + + return wp_parse_args($settings, $defaults); + } +} + +// Initialize the plugin +function wp_to_html_init() { + return WP_To_HTML::get_instance(); +} + +// Start the plugin +add_action('plugins_loaded', 'wp_to_html_init'); + +// Add settings link on plugin page +function wp_to_html_plugin_action_links($links) { + $settings_link = '' . __('Settings', 'wp-to-html') . ''; + array_unshift($links, $settings_link); + return $links; +} +add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'wp_to_html_plugin_action_links');