From e468202f9527e6e2e085ae909e327c12644f9256 Mon Sep 17 00:00:00 2001 From: rodolfomartinez Date: Mon, 2 Feb 2026 12:35:28 -0500 Subject: [PATCH] added maple performance caching plugin --- .../wordpress/maple-performance-wp/README.md | 228 +++ .../maple-performance-wp/css/index.php | 11 + .../inc/class-maple-admin.php | 837 +++++++++++ .../inc/class-maple-cache.php | 608 ++++++++ .../inc/class-maple-compat.php | 801 +++++++++++ .../inc/class-maple-optimize.php | 1234 +++++++++++++++++ .../maple-performance-wp/inc/index.php | 11 + .../wordpress/maple-performance-wp/index.php | 11 + .../maple-performance-wp/js/index.php | 11 + .../maple-performance.php | 539 +++++++ .../wordpress/maple-performance-wp/readme.txt | 142 ++ .../maple-performance-wp/uninstall.php | 68 + 12 files changed, 4501 insertions(+) create mode 100644 native/wordpress/maple-performance-wp/README.md create mode 100644 native/wordpress/maple-performance-wp/css/index.php create mode 100644 native/wordpress/maple-performance-wp/inc/class-maple-admin.php create mode 100644 native/wordpress/maple-performance-wp/inc/class-maple-cache.php create mode 100644 native/wordpress/maple-performance-wp/inc/class-maple-compat.php create mode 100644 native/wordpress/maple-performance-wp/inc/class-maple-optimize.php create mode 100644 native/wordpress/maple-performance-wp/inc/index.php create mode 100644 native/wordpress/maple-performance-wp/index.php create mode 100644 native/wordpress/maple-performance-wp/js/index.php create mode 100644 native/wordpress/maple-performance-wp/maple-performance.php create mode 100644 native/wordpress/maple-performance-wp/readme.txt create mode 100644 native/wordpress/maple-performance-wp/uninstall.php diff --git a/native/wordpress/maple-performance-wp/README.md b/native/wordpress/maple-performance-wp/README.md new file mode 100644 index 0000000..c7df4b8 --- /dev/null +++ b/native/wordpress/maple-performance-wp/README.md @@ -0,0 +1,228 @@ +# Maple Performance WP 🍁 + +A lightweight, privacy-focused WordPress performance plugin. No external dependencies, no tracking, no upsells. + +**Built by [Maple Open Tech](https://mapleopentech.ca) for Canadian businesses who care about data sovereignty.** + +## Features + +### Page Caching +- Static HTML file generation +- Gzip pre-compression +- Brotli pre-compression (if PHP extension installed) +- Smart cache invalidation on content updates +- Automatic exclusion for logged-in users + +### Asset Optimization +- **HTML**: Minification, comment removal +- **CSS**: Minification, aggregation, optional async loading +- **JavaScript**: Minification, aggregation (disabled by default for safety) +- **Google Fonts**: Combine multiple requests, add display:swap, optional deferred loading + +### Lazy Loading +- Native `loading="lazy"` for images +- Native `loading="lazy"` for iframes +- Exclude LCP/hero images from lazy loading + +### Extra Optimizations +- Remove WordPress emoji scripts +- Remove query strings from static resources +- DNS prefetch hints +- Preconnect to third-party domains + +### Smart Plugin Compatibility + +Maple Performance automatically detects these plugins and applies safe exclusions: + +| Plugin | Automatic Protections | +|--------|----------------------| +| **WooCommerce** | Cart/checkout/account pages excluded from cache, WooCommerce cookies bypass cache, cart fragments and checkout scripts protected | +| **LearnDash** | Lesson/topic/quiz pages excluded, progress tracking AJAX protected, quiz scripts excluded from aggregation | +| **WPForms** | Form validation scripts excluded, AJAX submissions protected | +| **Wordfence** | Security scripts excluded, login pages not cached, firewall bypass cookies respected | +| **Gravity Forms** | Form scripts excluded from aggregation | +| **Contact Form 7** | Form scripts excluded from aggregation | +| **Elementor** | Builder scripts excluded from aggregation | + +## Site Modes + +Select your site type and Maple Performance automatically applies safe defaults: + +| Site Type | JS Aggregate | CSS Defer | Expected Score | +|-----------|--------------|-----------|----------------| +| Brochure/Blog | ✅ Available | ✅ Available | 80-95 | +| WooCommerce | ❌ Disabled | ❌ Disabled | 70-85 | +| LearnDash | ❌ Disabled | ❌ Disabled | 70-85 | +| WooCommerce + LearnDash | ❌ Disabled | ❌ Disabled | 65-80 | + +## Installation + +### From GitHub +1. Download or clone this repository +2. Upload the `maple-performance-wp` folder to `/wp-content/plugins/` +3. Activate through WordPress admin +4. Go to Settings > Maple Performance +5. Select your site type and configure + +### From WordPress Admin +1. Go to Plugins > Add New +2. Upload the zip file +3. Activate and configure + +## Configuration + +### Brochure Sites (Maximum Performance) + +``` +Site Mode: Brochure +Cache: Enabled +HTML Minify: On +CSS Minify: On +CSS Aggregate: On +CSS Defer: On (optional) +JS Minify: On +JS Aggregate: On +Lazy Load: On +``` + +### WooCommerce Sites (Safe Defaults) + +``` +Site Mode: WooCommerce +Cache: Enabled (cart/checkout excluded) +HTML Minify: On +CSS Minify: On +CSS Aggregate: On +CSS Defer: Off +JS Minify: On +JS Aggregate: Off (protects checkout) +Lazy Load: On +``` + +### LearnDash Sites (Safe Defaults) + +``` +Site Mode: LearnDash +Cache: Enabled (lessons excluded for logged-in) +HTML Minify: On +CSS Minify: On +CSS Aggregate: On +CSS Defer: Off +JS Minify: On +JS Aggregate: Off (protects tracking) +Lazy Load: On +``` + +## Privacy + +This plugin: +- ✅ Processes everything locally on your server +- ✅ Makes zero external API calls +- ✅ Sends no data to third parties +- ✅ Has no tracking or analytics +- ✅ Has no premium upsells or nags +- ✅ Is fully open source (GPL-2.0) + +## Privacy & GDPR Compliance + +Maple Performance WP is designed with privacy as a core principle: + +| Aspect | Status | +|--------|--------| +| Personal data collection | ❌ None | +| Cookies set by plugin | ❌ None | +| External connections | ❌ None | +| Tracking/analytics | ❌ None | +| Third-party services | ❌ None | + +### What the plugin stores + +- **Page cache**: Static HTML copies of publicly-visible pages (same content any visitor sees) +- **Asset cache**: Aggregated/minified CSS and JS files +- **Settings**: Your plugin configuration (no personal data) + +### Cookie behavior + +The plugin **reads** existing cookies (WordPress login, WooCommerce cart) only to determine whether to serve cached content. It never sets, modifies, or transmits cookie data. + +### Privacy Policy + +The plugin automatically registers suggested privacy policy text with WordPress (Settings → Privacy) that you can include in your site's privacy policy. + +## Requirements + +- WordPress 5.9+ +- PHP 7.4+ +- Write access to `wp-content/cache/` + +## Cache Location + +Cached files are stored in: +``` +wp-content/cache/maple-performance/ +├── assets/ # Aggregated CSS/JS files +├── {domain}/ # Page cache by domain +│ └── {path}/ # Page cache by URL path +│ ├── https-index.html +│ ├── https-index.html.gz +│ └── https-index.html.br +``` + +## Hooks & Filters + +### Actions + +```php +// Clear all cache +do_action( 'maple_performance_clear_cache' ); + +// After cache is cleared +do_action( 'maple_performance_cache_cleared' ); +``` + +### Filters + +```php +// Modify settings programmatically +add_filter( 'maple_performance_settings', function( $settings ) { + $settings['cache_enabled'] = false; + return $settings; +}); + +// Exclude specific URLs from caching +add_filter( 'maple_performance_exclude_url', function( $exclude, $url ) { + if ( strpos( $url, '/members/' ) !== false ) { + return true; + } + return $exclude; +}, 10, 2 ); +``` + +## Comparison + +| Feature | Maple Performance | Autoptimize | Cache Enabler | WP Rocket | +|---------|------------------|-------------|---------------|-----------| +| Page Cache | ✅ | ❌ | ✅ | ✅ | +| CSS/JS Minify | ✅ | ✅ | ❌ | ✅ | +| CSS/JS Aggregate | ✅ | ✅ | ❌ | ✅ | +| Google Fonts Optimization | ✅ | ✅ | ❌ | ✅ | +| External Dependencies | ❌ None | ⚠️ Optional | ⚠️ CDN promo | ✅ Required | +| Tracking | ❌ None | ⚠️ News feed | ❌ None | ✅ License | +| Upsells | ❌ None | ✅ Yes | ✅ Yes | N/A (paid) | +| WooCommerce Safe Mode | ✅ | ❌ | ❌ | ✅ | +| LearnDash Safe Mode | ✅ | ❌ | ❌ | ❌ | +| Price | Free | Free | Free | $59/year | + +## Contributing + +Contributions are welcome! Please feel free to submit issues and pull requests. + +## License + +GPL-2.0-or-later - see [LICENSE](LICENSE) for details. + +## Credits + +Built by [Maple Open Tech](https://mapleopentech.ca) 🍁 + +Inspired by the core concepts from Autoptimize and Cache Enabler, rebuilt from scratch with privacy and safety in mind. diff --git a/native/wordpress/maple-performance-wp/css/index.php b/native/wordpress/maple-performance-wp/css/index.php new file mode 100644 index 0000000..4f5fef0 --- /dev/null +++ b/native/wordpress/maple-performance-wp/css/index.php @@ -0,0 +1,11 @@ +init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // Add admin menu + add_action( 'admin_menu', array( $this, 'add_menu' ) ); + + // Register settings + add_action( 'admin_init', array( $this, 'register_settings' ) ); + + // Admin bar cache clear button + add_action( 'admin_bar_menu', array( $this, 'add_admin_bar_button' ), 100 ); + + // Handle cache clear action + add_action( 'admin_init', array( $this, 'handle_cache_clear' ) ); + + // Admin notices + add_action( 'admin_notices', array( $this, 'admin_notices' ) ); + + // Admin styles + add_action( 'admin_enqueue_scripts', array( $this, 'admin_styles' ) ); + + // Plugin action links + add_filter( 'plugin_action_links_' . plugin_basename( MAPLE_PERF_FILE ), array( $this, 'action_links' ) ); + + // Privacy policy + add_action( 'admin_init', array( $this, 'add_privacy_policy_content' ) ); + } + + /** + * Add privacy policy content + */ + public function add_privacy_policy_content() { + if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { + return; + } + + $content = sprintf( + '

%s

%s

%s

%s

%s

%s

%s

%s

', + __( 'Maple Performance WP', 'maple-performance' ), + __( 'This site uses the Maple Performance WP plugin to improve page load times through caching and asset optimization.', 'maple-performance' ), + __( 'What personal data we collect and why', 'maple-performance' ), + __( 'Maple Performance WP does not collect, store, or process any personal data. The plugin caches publicly-visible page content to improve performance. By default, pages are not cached for logged-in users, ensuring that no user-specific content is stored in the cache.', 'maple-performance' ), + __( 'Cookies', 'maple-performance' ), + __( 'Maple Performance WP does not set any cookies. The plugin may read existing cookies (such as WordPress login cookies or WooCommerce cart cookies) solely to determine whether to serve a cached page or bypass the cache for dynamic content. No cookie data is stored or transmitted.', 'maple-performance' ), + __( 'Third-party services', 'maple-performance' ), + __( 'Maple Performance WP does not connect to any external services or transmit any data to third parties. All caching and optimization is performed locally on your web server.', 'maple-performance' ) + ); + + wp_add_privacy_policy_content( + 'Maple Performance WP', + wp_kses_post( $content ) + ); + } + + /** + * Add admin menu + */ + public function add_menu() { + add_options_page( + __( 'Maple Performance', 'maple-performance' ), + __( 'Maple Performance', 'maple-performance' ), + 'manage_options', + 'maple-performance', + array( $this, 'settings_page' ) + ); + } + + /** + * Register settings + */ + public function register_settings() { + register_setting( + 'maple_performance_settings', + 'maple_performance_settings', + array( $this, 'sanitize_settings' ) + ); + } + + /** + * Sanitize settings + */ + public function sanitize_settings( $input ) { + $sanitized = array(); + + // Site mode + $sanitized['site_mode'] = sanitize_text_field( $input['site_mode'] ?? 'brochure' ); + + // Google Fonts mode + $valid_font_modes = array( 'leave', 'remove', 'combine', 'defer' ); + $sanitized['google_fonts'] = in_array( $input['google_fonts'] ?? 'defer', $valid_font_modes ) + ? $input['google_fonts'] + : 'defer'; + + // Plugin compatibility + $sanitized['compat_auto_detect'] = ! empty( $input['compat_auto_detect'] ); + + // Manual plugin selection + $valid_compat_plugins = array( 'woocommerce', 'learndash', 'wordfence', 'wpforms', 'gutenberg_fse' ); + $sanitized['compat_plugins'] = array(); + if ( ! empty( $input['compat_plugins'] ) && is_array( $input['compat_plugins'] ) ) { + foreach ( $input['compat_plugins'] as $plugin ) { + if ( in_array( $plugin, $valid_compat_plugins ) ) { + $sanitized['compat_plugins'][] = $plugin; + } + } + } + + // Boolean settings + $booleans = array( + 'cache_enabled', 'cache_logged_in', 'cache_gzip', 'cache_brotli', + 'html_minify', 'html_remove_comments', + 'css_minify', 'css_aggregate', 'css_inline_aggregate', 'css_defer', + 'js_minify', 'js_aggregate', 'js_defer', 'js_exclude_jquery', + 'remove_emojis', 'remove_query_strings', 'dns_prefetch', + 'lazyload_images', 'lazyload_iframes', + 'local_font_display', + ); + + foreach ( $booleans as $key ) { + $sanitized[ $key ] = ! empty( $input[ $key ] ); + } + + // Integer settings + $sanitized['cache_expiry'] = absint( $input['cache_expiry'] ?? 0 ); + + // Array settings (textarea, one per line) + $arrays = array( + 'exclude_paths', 'exclude_cookies', 'exclude_js', 'exclude_css', + 'preconnect_domains', 'lazyload_exclude', 'preload_fonts', + ); + + foreach ( $arrays as $key ) { + $value = $input[ $key ] ?? ''; + if ( is_string( $value ) ) { + $lines = explode( "\n", $value ); + $sanitized[ $key ] = array_filter( array_map( function( $line ) { + // Sanitize each line - remove potentially dangerous characters + $line = trim( $line ); + $line = str_replace( array( "\0", "\r" ), '', $line ); + // For URLs/paths, use esc_url_raw for domains, sanitize_text_field for others + if ( strpos( $line, 'http' ) === 0 || strpos( $line, '//' ) === 0 ) { + return esc_url_raw( $line ); + } + return sanitize_text_field( $line ); + }, $lines ) ); + } else { + $sanitized[ $key ] = array(); + } + } + + // Clear cache when settings change + maple_performance()->clear_cache(); + + return $sanitized; + } + + /** + * Add admin bar button + */ + public function add_admin_bar_button( $wp_admin_bar ) { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $wp_admin_bar->add_node( array( + 'id' => 'maple-performance', + 'title' => '' . __( 'Maple Cache', 'maple-performance' ), + 'href' => '#', + ) ); + + $wp_admin_bar->add_node( array( + 'parent' => 'maple-performance', + 'id' => 'maple-clear-cache', + 'title' => __( 'Clear All Cache', 'maple-performance' ), + 'href' => wp_nonce_url( admin_url( 'admin.php?action=maple_clear_cache' ), 'maple_clear_cache' ), + ) ); + + $wp_admin_bar->add_node( array( + 'parent' => 'maple-performance', + 'id' => 'maple-settings', + 'title' => __( 'Settings', 'maple-performance' ), + 'href' => admin_url( 'options-general.php?page=maple-performance' ), + ) ); + + // Show cache stats - use transient to avoid filesystem scan on every page load + $stats = get_transient( 'maple_perf_cache_stats' ); + if ( false === $stats ) { + // Only calculate if not cached, with a 5-minute expiry + $stats = array( + 'count' => Maple_Performance_Cache::get_cache_count(), + 'size' => Maple_Performance_Cache::get_cache_size(), + ); + set_transient( 'maple_perf_cache_stats', $stats, 5 * MINUTE_IN_SECONDS ); + } + + $wp_admin_bar->add_node( array( + 'parent' => 'maple-performance', + 'id' => 'maple-cache-stats', + 'title' => sprintf( __( 'Cache: %d pages (%s)', 'maple-performance' ), $stats['count'], size_format( $stats['size'] ) ), + 'href' => admin_url( 'options-general.php?page=maple-performance' ), + ) ); + } + + /** + * Handle cache clear action + */ + public function handle_cache_clear() { + if ( ! isset( $_GET['action'] ) || $_GET['action'] !== 'maple_clear_cache' ) { + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( __( 'Unauthorized', 'maple-performance' ) ); + } + + if ( ! wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'maple_clear_cache' ) ) { + wp_die( __( 'Invalid nonce', 'maple-performance' ) ); + } + + maple_performance()->clear_cache(); + + // Redirect back with notice + $redirect = remove_query_arg( array( 'action', '_wpnonce' ), wp_get_referer() ); + $redirect = add_query_arg( 'maple_cache_cleared', '1', $redirect ); + + wp_safe_redirect( $redirect ); + exit; + } + + /** + * Admin notices + */ + public function admin_notices() { + // Show activation conflict notice (only once, immediately after activation) + $activation_conflicts = get_transient( 'maple_perf_activation_conflict' ); + if ( false !== $activation_conflicts && is_array( $activation_conflicts ) ) { + delete_transient( 'maple_perf_activation_conflict' ); + + echo '
'; + echo '

' . esc_html__( '⚠️ Maple Performance WP - Important!', 'maple-performance' ) . '

'; + echo '

' . sprintf( + esc_html__( 'Another caching plugin is already active: %s', 'maple-performance' ), + '' . esc_html( implode( ', ', $activation_conflicts ) ) . '' + ) . '

'; + echo '

' . esc_html__( 'Running multiple caching plugins can cause site errors, blank pages, or performance issues.', 'maple-performance' ) . '

'; + echo '

'; + echo '' . esc_html__( 'Choose which plugin to keep', 'maple-performance' ) . ' '; + echo '' . esc_html__( 'Go to Settings', 'maple-performance' ) . ''; + echo '

'; + echo '
'; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display check + if ( isset( $_GET['maple_cache_cleared'] ) ) { + echo '
'; + echo '

' . esc_html__( 'Maple Performance: Cache cleared successfully.', 'maple-performance' ) . '

'; + echo '
'; + } + + // Check if settings were saved + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display check + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + if ( isset( $_GET['settings-updated'] ) && $page === 'maple-performance' ) { + echo '
'; + echo '

' . esc_html__( 'Maple Performance: Settings saved and cache cleared.', 'maple-performance' ) . '

'; + echo '
'; + } + } + + /** + * Admin styles + */ + public function admin_styles( $hook ) { + if ( $hook !== 'settings_page_maple-performance' ) { + return; + } + + wp_add_inline_style( 'common', ' + .maple-settings { max-width: 900px; } + .maple-settings h2 { border-bottom: 1px solid #ccc; padding-bottom: 10px; margin-top: 30px; } + .maple-settings h2:first-of-type { margin-top: 10px; } + .maple-settings table { margin-bottom: 20px; } + .maple-settings .description { color: #666; font-style: italic; } + .maple-settings textarea { width: 100%; max-width: 400px; } + .maple-settings .site-mode-card { + border: 2px solid #ddd; + padding: 15px; + margin: 10px 0; + border-radius: 5px; + cursor: pointer; + } + .maple-settings .site-mode-card:hover { border-color: #2271b1; } + .maple-settings .site-mode-card.selected { border-color: #2271b1; background: #f0f7fc; } + .maple-settings .site-mode-card h4 { margin: 0 0 5px; } + .maple-settings .warning { color: #d63638; } + .maple-settings .safe { color: #00a32a; } + .maple-cache-stats { + background: #f0f0f1; + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + } + .maple-cache-stats strong { font-size: 1.2em; } + .maple-detected-plugins { + background: #f9f9f9; + border: 1px solid #ddd; + border-radius: 5px; + padding: 15px 20px; + margin-bottom: 25px; + } + .maple-detected-plugins h3 { margin-top: 0; } + .maple-detected-plugins table { margin-top: 15px; } + .maple-detected-plugins ul li { font-size: 13px; color: #555; } + ' ); + } + + /** + * Plugin action links + */ + public function action_links( $links ) { + $settings_link = '' . __( 'Settings', 'maple-performance' ) . ''; + $clear_link = '' . __( 'Clear Cache', 'maple-performance' ) . ''; + + array_unshift( $links, $settings_link, $clear_link ); + + return $links; + } + + /** + * Settings page + */ + public function settings_page() { + $settings = maple_performance()->settings; + + // Get cache stats + $cache_count = Maple_Performance_Cache::get_cache_count(); + $cache_size = Maple_Performance_Cache::get_cache_size(); + ?> +
+

+ +

+ +
+ + +    + + + +
+ + get_detected() : array(); + ?> + +
+ + +

+

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

+ Manual selection below is recommended for reliability.', 'maple-performance' ); ?> +

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

+ Tip: Select plugins you have installed even if not detected. Detection can miss plugins due to naming changes between versions.', 'maple-performance' ); ?> +

+
+ + + +

+

+ + + + + + +
+ +

+ Brochure: Full optimization enabled. WooCommerce/LearnDash: Conservative settings to protect checkout and lesson tracking.', 'maple-performance' ); ?> +

+
+ +

+ + get_caching_conflicts(); + if ( ! empty( $caching_conflicts ) ) { + echo '
'; + echo '

' . esc_html__( '⚠️ Another caching plugin detected:', 'maple-performance' ) . ' '; + echo esc_html( implode( ', ', $caching_conflicts ) ) . '

'; + echo '

' . esc_html__( 'Consider disabling Page Cache below and using only the CSS/JS/HTML optimizations to avoid conflicts.', 'maple-performance' ) . '

'; + echo '
'; + } + } + ?> + + + + + + + + + + + + + + +
+ +
+ + +
+ +
+ +
+ +

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

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

+
+ +

+
+ +

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

+
+ +
+ +

+
+ +

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

+
+ +

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

+ Defer is recommended - loads fonts without blocking page render.', 'maple-performance' ); ?> +

+
+ +

+ +

+
+ +

+ +
+ Example: /wp-content/themes/yourtheme/fonts/font-name.woff2', 'maple-performance' ); ?> +

+
+ +
+ +
+ +
+ +

+
+ +

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

+
+ +

+
+ + + +
+ +
+

+ v | + Maple Open Tech | + 🍁 +

+
+ settings = maple_performance()->settings; + $this->init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // Start output buffering early + add_action( 'template_redirect', array( $this, 'start_buffering' ), -999 ); + + // Cache clearing hooks + add_action( 'save_post', array( $this, 'clear_post_cache' ), 10, 1 ); + add_action( 'delete_post', array( $this, 'clear_post_cache' ), 10, 1 ); + add_action( 'wp_trash_post', array( $this, 'clear_post_cache' ), 10, 1 ); + add_action( 'comment_post', array( $this, 'clear_post_cache_by_comment' ), 10, 2 ); + add_action( 'edit_comment', array( $this, 'clear_post_cache_by_comment' ), 10, 1 ); + add_action( 'switch_theme', array( __CLASS__, 'clear_all' ) ); + add_action( 'activated_plugin', array( __CLASS__, 'clear_all' ) ); + add_action( 'deactivated_plugin', array( __CLASS__, 'clear_all' ) ); + add_action( 'update_option_permalink_structure', array( __CLASS__, 'clear_all' ) ); + + // WooCommerce hooks + if ( class_exists( 'WooCommerce' ) ) { + add_action( 'woocommerce_product_set_stock', array( $this, 'clear_product_cache' ) ); + add_action( 'woocommerce_variation_set_stock', array( $this, 'clear_product_cache' ) ); + } + } + + /** + * Start output buffering + */ + public function start_buffering() { + // Check if we should cache this request + if ( maple_performance()->is_excluded() ) { + return; + } + + // Check if cached version exists + $cache_file = $this->get_cache_file_path(); + + if ( $this->serve_cache( $cache_file ) ) { + exit; + } + + // Start buffering for cache creation + ob_start( array( $this, 'process_buffer' ) ); + } + + /** + * Process output buffer and create cache + */ + public function process_buffer( $buffer ) { + // Don't cache empty or error pages + if ( empty( $buffer ) || http_response_code() !== 200 ) { + return $buffer; + } + + // Don't cache if contains certain markers + if ( $this->should_skip_caching( $buffer ) ) { + return $buffer; + } + + // Write cache file + $this->write_cache( $buffer ); + + return $buffer; + } + + /** + * Check if page should skip caching based on content + */ + private function should_skip_caching( $buffer ) { + // Skip if contains no-cache markers + $skip_markers = array( + '', + '', + 'woocommerce-cart', + 'woocommerce-checkout', + ); + + foreach ( $skip_markers as $marker ) { + if ( strpos( $buffer, $marker ) !== false ) { + return true; + } + } + + return false; + } + + /** + * Get cache file path for current request + */ + private function get_cache_file_path( $url = null ) { + if ( null === $url ) { + $url = $this->get_current_url(); + } + + $parsed = parse_url( $url ); + $host = $parsed['host'] ?? ''; + $path = $parsed['path'] ?? '/'; + + // Sanitize host - only allow alphanumeric, dots, and hyphens + $host = preg_replace( '/[^a-zA-Z0-9.-]/', '', $host ); + + // Sanitize path - remove any path traversal attempts + $path = str_replace( array( '..', "\0" ), '', $path ); + $path = preg_replace( '/[^a-zA-Z0-9\/_-]/', '', $path ); + $path = preg_replace( '#/+#', '/', $path ); // Collapse multiple slashes + $path = rtrim( $path, '/' ); + + if ( empty( $path ) ) { + $path = '/index'; + } + + // Limit path depth to prevent excessive directory creation + $path_parts = explode( '/', trim( $path, '/' ) ); + if ( count( $path_parts ) > 10 ) { + $path_parts = array_slice( $path_parts, 0, 10 ); + $path = '/' . implode( '/', $path_parts ); + } + + // Build cache directory structure + $cache_dir = MAPLE_PERF_CACHE_DIR . $host . $path . '/'; + + // Verify the resolved path is within cache directory + $real_cache_base = realpath( MAPLE_PERF_CACHE_DIR ); + if ( $real_cache_base ) { + // Use dirname to check parent since $cache_dir may not exist yet + $parent_dir = dirname( $cache_dir ); + while ( ! is_dir( $parent_dir ) && $parent_dir !== dirname( $parent_dir ) ) { + $parent_dir = dirname( $parent_dir ); + } + + if ( is_dir( $parent_dir ) ) { + $real_parent = realpath( $parent_dir ); + if ( $real_parent && strpos( $real_parent, $real_cache_base ) !== 0 ) { + // Path escapes cache directory - return safe fallback + return MAPLE_PERF_CACHE_DIR . 'fallback/https-index.html'; + } + } + } + + // Determine cache file name + $is_https = is_ssl() ? 'https' : 'http'; + $cache_file = $cache_dir . $is_https . '-index.html'; + + return $cache_file; + } + + /** + * Get current URL + */ + private function get_current_url() { + $scheme = is_ssl() ? 'https' : 'http'; + + // Sanitize HTTP_HOST + $host = isset( $_SERVER['HTTP_HOST'] ) ? $_SERVER['HTTP_HOST'] : ''; + $host = preg_replace( '/[^a-zA-Z0-9.-]/', '', $host ); + + // Sanitize REQUEST_URI + $uri = isset( $_SERVER['REQUEST_URI'] ) ? $_SERVER['REQUEST_URI'] : '/'; + $uri = filter_var( $uri, FILTER_SANITIZE_URL ); + + // Remove query strings for cache key + $uri = strtok( $uri, '?' ); + + // Remove any null bytes or path traversal + $uri = str_replace( array( "\0", '..' ), '', $uri ); + + return $scheme . '://' . $host . $uri; + } + + /** + * Serve cached file if exists + */ + private function serve_cache( $cache_file ) { + // Check for gzipped version first + if ( $this->settings['cache_gzip'] && $this->client_accepts_gzip() ) { + $gzip_file = $cache_file . '.gz'; + if ( file_exists( $gzip_file ) && $this->is_cache_valid( $gzip_file ) ) { + header( 'Content-Encoding: gzip' ); + header( 'Content-Type: text/html; charset=UTF-8' ); + header( 'X-Maple-Cache: HIT (gzip)' ); + readfile( $gzip_file ); + return true; + } + } + + // Check for brotli version + if ( $this->settings['cache_brotli'] && $this->client_accepts_brotli() ) { + $br_file = $cache_file . '.br'; + if ( file_exists( $br_file ) && $this->is_cache_valid( $br_file ) ) { + header( 'Content-Encoding: br' ); + header( 'Content-Type: text/html; charset=UTF-8' ); + header( 'X-Maple-Cache: HIT (brotli)' ); + readfile( $br_file ); + return true; + } + } + + // Serve uncompressed + if ( file_exists( $cache_file ) && $this->is_cache_valid( $cache_file ) ) { + header( 'Content-Type: text/html; charset=UTF-8' ); + header( 'X-Maple-Cache: HIT' ); + readfile( $cache_file ); + return true; + } + + return false; + } + + /** + * Check if cache file is still valid + */ + private function is_cache_valid( $cache_file ) { + // No expiry set + if ( empty( $this->settings['cache_expiry'] ) || $this->settings['cache_expiry'] === 0 ) { + return true; + } + + $file_age = time() - filemtime( $cache_file ); + $max_age = $this->settings['cache_expiry'] * HOUR_IN_SECONDS; + + return $file_age < $max_age; + } + + /** + * Check if client accepts gzip + */ + private function client_accepts_gzip() { + $encoding = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''; + return strpos( $encoding, 'gzip' ) !== false; + } + + /** + * Check if client accepts brotli + */ + private function client_accepts_brotli() { + $encoding = $_SERVER['HTTP_ACCEPT_ENCODING'] ?? ''; + return strpos( $encoding, 'br' ) !== false; + } + + /** + * Write cache file + */ + private function write_cache( $content ) { + $cache_file = $this->get_cache_file_path(); + $cache_dir = dirname( $cache_file ); + + // Create directory if needed + if ( ! file_exists( $cache_dir ) ) { + wp_mkdir_p( $cache_dir ); + } + + // Verify we're still within cache directory (paranoid check) + $real_cache_base = realpath( MAPLE_PERF_CACHE_DIR ); + $real_cache_dir = realpath( $cache_dir ); + + if ( false === $real_cache_base || false === $real_cache_dir ) { + return; + } + + if ( strpos( $real_cache_dir, $real_cache_base ) !== 0 ) { + return; + } + + // Add cache signature + $timestamp = gmdate( 'D, d M Y H:i:s' ) . ' GMT'; + $signature = "\n"; + $content .= $signature; + + // Write HTML file with proper permissions + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $cache_file, $content ); + if ( file_exists( $cache_file ) ) { + chmod( $cache_file, 0644 ); + } + + // Create gzipped version + if ( $this->settings['cache_gzip'] && function_exists( 'gzencode' ) ) { + $gzip_content = gzencode( $content, 9 ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $cache_file . '.gz', $gzip_content ); + if ( file_exists( $cache_file . '.gz' ) ) { + chmod( $cache_file . '.gz', 0644 ); + } + } + + // Create brotli version + if ( $this->settings['cache_brotli'] && function_exists( 'brotli_compress' ) ) { + $br_content = brotli_compress( $content ); + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $cache_file . '.br', $br_content ); + if ( file_exists( $cache_file . '.br' ) ) { + chmod( $cache_file . '.br', 0644 ); + } + } + } + + /** + * Clear cache for a specific post + */ + public function clear_post_cache( $post_id ) { + // Don't clear for autosaves or revisions + if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) { + return; + } + + $post = get_post( $post_id ); + if ( ! $post || $post->post_status !== 'publish' ) { + return; + } + + // Clear post URL + $url = get_permalink( $post_id ); + $this->clear_url_cache( $url ); + + // Clear home page + $this->clear_url_cache( home_url( '/' ) ); + + // Clear archive pages + $post_type = get_post_type( $post_id ); + $archive_url = get_post_type_archive_link( $post_type ); + if ( $archive_url ) { + $this->clear_url_cache( $archive_url ); + } + + // Clear category/tag archives - limit to prevent performance issues + $taxonomies = get_object_taxonomies( $post_type ); + $terms_cleared = 0; + $max_terms = 50; // Limit term cache clearing to prevent slowdown + + foreach ( $taxonomies as $taxonomy ) { + if ( $terms_cleared >= $max_terms ) { + break; + } + + $terms = get_the_terms( $post_id, $taxonomy ); + if ( $terms && ! is_wp_error( $terms ) ) { + foreach ( $terms as $term ) { + if ( $terms_cleared >= $max_terms ) { + break; + } + + $term_url = get_term_link( $term ); + if ( ! is_wp_error( $term_url ) ) { + $this->clear_url_cache( $term_url ); + $terms_cleared++; + } + } + } + } + + // Clear stats transient since cache changed + delete_transient( 'maple_perf_cache_stats' ); + } + + /** + * Clear cache by comment + */ + public function clear_post_cache_by_comment( $comment_id, $comment_approved = null ) { + $comment = get_comment( $comment_id ); + if ( $comment ) { + $this->clear_post_cache( $comment->comment_post_ID ); + } + } + + /** + * Clear product cache (WooCommerce) + */ + public function clear_product_cache( $product ) { + if ( is_numeric( $product ) ) { + $product_id = $product; + } else { + $product_id = $product->get_id(); + } + + $this->clear_post_cache( $product_id ); + } + + /** + * Clear cache for a specific URL + */ + public function clear_url_cache( $url ) { + $cache_file = $this->get_cache_file_path( $url ); + + if ( file_exists( $cache_file ) ) { + @unlink( $cache_file ); + } + if ( file_exists( $cache_file . '.gz' ) ) { + @unlink( $cache_file . '.gz' ); + } + if ( file_exists( $cache_file . '.br' ) ) { + @unlink( $cache_file . '.br' ); + } + + // Also clear https version if http was cleared and vice versa + $alt_file = str_replace( + array( '/http-index.html', '/https-index.html' ), + array( '/https-index.html', '/http-index.html' ), + $cache_file + ); + + if ( file_exists( $alt_file ) ) { + @unlink( $alt_file ); + } + if ( file_exists( $alt_file . '.gz' ) ) { + @unlink( $alt_file . '.gz' ); + } + if ( file_exists( $alt_file . '.br' ) ) { + @unlink( $alt_file . '.br' ); + } + } + + /** + * Clear all cache + */ + public static function clear_all() { + self::recursive_delete( MAPLE_PERF_CACHE_DIR ); + + // Recreate directories + wp_mkdir_p( MAPLE_PERF_CACHE_DIR ); + wp_mkdir_p( MAPLE_PERF_CACHE_DIR . 'assets/' ); + + // Add index.php files with ABSPATH check + $index = " $max_iterations ) { + break; // Safety limit reached + } + + $file_path = $file->getRealPath(); + + // Verify each file is within allowed directory + if ( strpos( $file_path, $real_dir ) !== 0 ) { + continue; + } + + if ( $file->isDir() ) { + @rmdir( $file_path ); + } else { + @unlink( $file_path ); + } + } + } catch ( Exception $e ) { + // Handle iterator exceptions gracefully + return; + } + } + + /** + * Get cache size + */ + public static function get_cache_size() { + $size = 0; + + if ( ! is_dir( MAPLE_PERF_CACHE_DIR ) ) { + return $size; + } + + // Limit iterations to prevent runaway on huge cache directories + $max_iterations = 10000; + $iterations = 0; + + try { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( MAPLE_PERF_CACHE_DIR, RecursiveDirectoryIterator::SKIP_DOTS ) + ); + + foreach ( $files as $file ) { + if ( ++$iterations > $max_iterations ) { + break; // Safety limit reached + } + + if ( $file->isFile() ) { + $size += $file->getSize(); + } + } + } catch ( Exception $e ) { + // Handle iterator exceptions gracefully + return $size; + } + + return $size; + } + + /** + * Get number of cached files + */ + public static function get_cache_count() { + $count = 0; + + if ( ! is_dir( MAPLE_PERF_CACHE_DIR ) ) { + return $count; + } + + // Limit iterations to prevent runaway on huge cache directories + $max_iterations = 10000; + $iterations = 0; + + try { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( MAPLE_PERF_CACHE_DIR, RecursiveDirectoryIterator::SKIP_DOTS ) + ); + + foreach ( $files as $file ) { + if ( ++$iterations > $max_iterations ) { + break; // Safety limit reached + } + + if ( $file->isFile() && pathinfo( $file, PATHINFO_EXTENSION ) === 'html' ) { + $count++; + } + } + } catch ( Exception $e ) { + // Handle iterator exceptions gracefully + return $count; + } + + return $count; + } +} diff --git a/native/wordpress/maple-performance-wp/inc/class-maple-compat.php b/native/wordpress/maple-performance-wp/inc/class-maple-compat.php new file mode 100644 index 0000000..42d425f --- /dev/null +++ b/native/wordpress/maple-performance-wp/inc/class-maple-compat.php @@ -0,0 +1,801 @@ +settings = maple_performance()->settings; + $this->determine_enabled_plugins(); + $this->init_hooks(); + } + + /** + * Determine which plugins should have compatibility rules applied + */ + private function determine_enabled_plugins() { + // If auto-detect is enabled, detect plugins + if ( ! empty( $this->settings['compat_auto_detect'] ) ) { + $this->detect_plugins(); + $this->enabled = $this->detected; + } else { + // Use manual selection + $manual = $this->settings['compat_plugins'] ?? array(); + foreach ( $manual as $plugin ) { + $this->enabled[ $plugin ] = true; + } + } + } + + /** + * Detect active plugins (used when auto-detect is enabled) + */ + private function detect_plugins() { + // WooCommerce + if ( class_exists( 'WooCommerce' ) || defined( 'WC_PLUGIN_FILE' ) ) { + $this->detected['woocommerce'] = true; + } + + // LearnDash + if ( defined( 'LEARNDASH_VERSION' ) || class_exists( 'SFWD_LMS' ) ) { + $this->detected['learndash'] = true; + } + + // WPForms + if ( defined( 'WPFORMS_VERSION' ) || class_exists( 'WPForms' ) ) { + $this->detected['wpforms'] = true; + } + + // Wordfence + if ( defined( 'WORDFENCE_VERSION' ) || class_exists( 'wordfence' ) ) { + $this->detected['wordfence'] = true; + } + + // Gutenberg FSE / Block Themes + if ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) { + $this->detected['gutenberg_fse'] = true; + } + + // Gravity Forms + if ( class_exists( 'GFForms' ) || defined( 'GF_MIN_WP_VERSION' ) ) { + $this->detected['gravityforms'] = true; + } + + // Contact Form 7 + if ( defined( 'WPCF7_VERSION' ) || class_exists( 'WPCF7' ) ) { + $this->detected['cf7'] = true; + } + + // Elementor + if ( defined( 'ELEMENTOR_VERSION' ) ) { + $this->detected['elementor'] = true; + } + + // === Caching plugin conflict detection === + + // WP Rocket + if ( defined( 'WP_ROCKET_VERSION' ) ) { + $this->detected['wp_rocket'] = true; + } + + // W3 Total Cache + if ( defined( 'W3TC' ) || class_exists( 'W3_Plugin_TotalCache' ) ) { + $this->detected['w3tc'] = true; + } + + // LiteSpeed Cache + if ( defined( 'LSCWP_V' ) || class_exists( 'LiteSpeed_Cache' ) ) { + $this->detected['litespeed'] = true; + } + + // WP Super Cache + if ( defined( 'WPCACHEHOME' ) || function_exists( 'wp_cache_phase2' ) ) { + $this->detected['wp_super_cache'] = true; + } + + // WP Fastest Cache + if ( class_exists( 'WpFastestCache' ) || defined( 'WPFC_WP_CONTENT_BASENAME' ) ) { + $this->detected['wp_fastest_cache'] = true; + } + + // Autoptimize + if ( class_exists( 'autoptimizeMain' ) || defined( 'AUTOPTIMIZE_PLUGIN_VERSION' ) ) { + $this->detected['autoptimize'] = true; + } + + // Cache Enabler + if ( class_exists( 'Cache_Enabler' ) || defined( 'CACHE_ENABLER_VERSION' ) ) { + $this->detected['cache_enabler'] = true; + } + + // Hummingbird + if ( class_exists( 'WP_Hummingbird' ) || defined( 'WPHB_VERSION' ) ) { + $this->detected['hummingbird'] = true; + } + + // Breeze (Cloudways) + if ( class_exists( 'Breeze_Admin' ) || defined( 'BREEZE_VERSION' ) ) { + $this->detected['breeze'] = true; + } + + // SG Optimizer (SiteGround) + if ( class_exists( 'SiteGround_Optimizer' ) || defined( 'SG_OPTIMIZER_VERSION' ) ) { + $this->detected['sg_optimizer'] = true; + } + + // Swift Performance + if ( class_exists( 'Swift_Performance' ) || defined( 'FLAVOR_FLAVOR' ) ) { + $this->detected['swift_performance'] = true; + } + + // Comet Cache + if ( class_exists( 'WebSharks\\CometCache\\Classes\\Plugin' ) || defined( 'COMET_CACHE_VERSION' ) ) { + $this->detected['comet_cache'] = true; + } + + // Powered Cache + if ( defined( 'POWERED_CACHE_VERSION' ) ) { + $this->detected['powered_cache'] = true; + } + + // Perfmatters + if ( class_exists( 'Perfmatters' ) || defined( 'PERFMATTERS_VERSION' ) ) { + $this->detected['perfmatters'] = true; + } + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // Only apply filters if at least one plugin is enabled + if ( empty( $this->enabled ) ) { + // Still check for caching conflicts in admin + add_action( 'admin_notices', array( $this, 'conflict_notices' ) ); + return; + } + + // Filter exclusions based on enabled plugins + add_filter( 'maple_performance_js_exclusions', array( $this, 'add_js_exclusions' ) ); + add_filter( 'maple_performance_css_exclusions', array( $this, 'add_css_exclusions' ) ); + add_filter( 'maple_performance_path_exclusions', array( $this, 'add_path_exclusions' ) ); + add_filter( 'maple_performance_cookie_exclusions', array( $this, 'add_cookie_exclusions' ) ); + + // Add admin notice for conflicts + add_action( 'admin_notices', array( $this, 'conflict_notices' ) ); + + // WooCommerce specific hooks + if ( $this->is_enabled( 'woocommerce' ) ) { + $this->init_woocommerce_compat(); + } + + // LearnDash specific hooks + if ( $this->is_enabled( 'learndash' ) ) { + $this->init_learndash_compat(); + } + + // WPForms specific hooks + if ( $this->is_enabled( 'wpforms' ) ) { + $this->init_wpforms_compat(); + } + + // Wordfence specific hooks + if ( $this->is_enabled( 'wordfence' ) ) { + $this->init_wordfence_compat(); + } + + // Gutenberg FSE specific hooks + if ( $this->is_enabled( 'gutenberg_fse' ) ) { + $this->init_gutenberg_fse_compat(); + } + } + + /** + * Check if plugin compatibility is enabled (either via auto-detect or manual) + */ + public function is_enabled( $plugin ) { + return ! empty( $this->enabled[ $plugin ] ); + } + + /** + * Check if plugin was detected (for display purposes) + */ + public function is_detected( $plugin ) { + return ! empty( $this->detected[ $plugin ] ); + } + + /** + * Get all detected plugins + */ + public function get_detected() { + // Always run detection for display in admin + if ( empty( $this->detected ) ) { + $this->detect_plugins(); + } + return $this->detected; + } + + /** + * Get all enabled plugins + */ + public function get_enabled() { + return $this->enabled; + } + + /** + * Add JS exclusions for enabled plugins + */ + public function add_js_exclusions( $exclusions ) { + // WooCommerce + if ( $this->is_enabled( 'woocommerce' ) ) { + $exclusions = array_merge( $exclusions, array( + 'woocommerce', + 'wc-', + 'wc_', + 'jquery-blockui', + 'selectWoo', + 'select2', + 'js-cookie', + 'cart-fragments', + 'checkout', + 'add-to-cart', + 'payment', + 'stripe', + 'paypal', + 'square', + 'braintree', + ) ); + } + + // LearnDash + if ( $this->is_enabled( 'learndash' ) ) { + $exclusions = array_merge( $exclusions, array( + 'learndash', + 'sfwd-', + 'sfwd_', + 'ld-', + 'ld_', + 'ldlms', + 'quiz', + 'wpProQuiz', + ) ); + } + + // WPForms + if ( $this->is_enabled( 'wpforms' ) ) { + $exclusions = array_merge( $exclusions, array( + 'wpforms', + 'wpforms-', + 'jquery-validation', + 'mailcheck', + 'inputmask', + ) ); + } + + // Wordfence + if ( $this->is_enabled( 'wordfence' ) ) { + $exclusions = array_merge( $exclusions, array( + 'wordfence', + 'wf-', + 'wfls-', + ) ); + } + + // Gutenberg FSE / Block Themes + if ( $this->is_enabled( 'gutenberg_fse' ) ) { + $exclusions = array_merge( $exclusions, array( + 'wp-block-', + 'wp-edit-', + ) ); + } + + // Gravity Forms + if ( $this->is_enabled( 'gravityforms' ) ) { + $exclusions = array_merge( $exclusions, array( + 'gform', + 'gravityforms', + 'gf_', + ) ); + } + + // Contact Form 7 + if ( $this->is_enabled( 'cf7' ) ) { + $exclusions = array_merge( $exclusions, array( + 'contact-form-7', + 'wpcf7', + ) ); + } + + // Elementor + if ( $this->is_enabled( 'elementor' ) ) { + $exclusions = array_merge( $exclusions, array( + 'elementor-', + 'elementor_', + ) ); + } + + return array_unique( $exclusions ); + } + + /** + * Add CSS exclusions for enabled plugins + */ + public function add_css_exclusions( $exclusions ) { + // LearnDash - Focus Mode CSS should load normally + if ( $this->is_enabled( 'learndash' ) ) { + $exclusions = array_merge( $exclusions, array( + 'learndash-front', + 'sfwd-', + ) ); + } + + // WPForms + if ( $this->is_enabled( 'wpforms' ) ) { + $exclusions = array_merge( $exclusions, array( + 'wpforms', + ) ); + } + + // Gutenberg FSE / Block Themes - protect global styles + if ( $this->is_enabled( 'gutenberg_fse' ) ) { + $exclusions = array_merge( $exclusions, array( + 'global-styles', + 'wp-block-', + 'core-block-', + ) ); + } + + return array_unique( $exclusions ); + } + + /** + * Add path exclusions for enabled plugins + */ + public function add_path_exclusions( $exclusions ) { + // WooCommerce + if ( $this->is_enabled( 'woocommerce' ) ) { + $exclusions = array_merge( $exclusions, array( + '/cart/', + '/cart', + '/checkout/', + '/checkout', + '/my-account/', + '/my-account', + '/wc-api/', + '/order-received/', + '/order-pay/', + '/view-order/', + '/add-to-cart=', + '?add-to-cart=', + '?remove_item=', + '?removed_item=', + ) ); + } + + // LearnDash + if ( $this->is_enabled( 'learndash' ) ) { + $exclusions = array_merge( $exclusions, array( + '/lessons/', + '/topic/', + '/quiz/', + '/quizzes/', + '/certificates/', + '/sfwd-', + ) ); + } + + // Wordfence + if ( $this->is_enabled( 'wordfence' ) ) { + $exclusions = array_merge( $exclusions, array( + '/wp-login.php', + '?wfls-', + ) ); + } + + return array_unique( $exclusions ); + } + + /** + * Add cookie exclusions for enabled plugins + */ + public function add_cookie_exclusions( $exclusions ) { + // WooCommerce + if ( $this->is_enabled( 'woocommerce' ) ) { + $exclusions = array_merge( $exclusions, array( + 'woocommerce_items_in_cart', + 'woocommerce_cart_hash', + 'wp_woocommerce_session_', + 'woocommerce_recently_viewed', + ) ); + } + + // Wordfence + if ( $this->is_enabled( 'wordfence' ) ) { + $exclusions = array_merge( $exclusions, array( + 'wfCBLBypass', + 'wf_loginalerted_', + ) ); + } + + return array_unique( $exclusions ); + } + + /** + * Initialize WooCommerce compatibility + */ + private function init_woocommerce_compat() { + // Don't cache cart fragments AJAX + add_action( 'wc_ajax_get_refreshed_fragments', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_add_to_cart', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_remove_from_cart', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_apply_coupon', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_remove_coupon', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_update_shipping_method', array( $this, 'disable_caching' ), 1 ); + add_action( 'wc_ajax_checkout', array( $this, 'disable_caching' ), 1 ); + + // Clear cache on stock changes + add_action( 'woocommerce_product_set_stock', array( $this, 'clear_product_cache' ) ); + add_action( 'woocommerce_variation_set_stock', array( $this, 'clear_product_cache' ) ); + + // Clear cache on order status changes (affects stock) + add_action( 'woocommerce_order_status_changed', array( $this, 'clear_on_order_status' ), 10, 3 ); + + // Exclude WooCommerce pages from optimization + add_filter( 'maple_performance_exclude_optimization', array( $this, 'exclude_woo_pages' ) ); + } + + /** + * Initialize LearnDash compatibility + */ + private function init_learndash_compat() { + // Don't cache any LearnDash AJAX + add_action( 'wp_ajax_learndash_mark_complete', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_ld_adv_quiz_result', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_wpProQuiz_admin_ajax', array( $this, 'disable_caching' ), 1 ); + + // Clear cache on course enrollment/completion + add_action( 'learndash_course_completed', array( $this, 'clear_learndash_user_cache' ), 10, 1 ); + add_action( 'learndash_lesson_completed', array( $this, 'clear_learndash_user_cache' ), 10, 1 ); + add_action( 'learndash_topic_completed', array( $this, 'clear_learndash_user_cache' ), 10, 1 ); + add_action( 'learndash_quiz_completed', array( $this, 'clear_learndash_user_cache' ), 10, 2 ); + + // Clear cache when user is enrolled + add_action( 'learndash_update_course_access', array( $this, 'clear_course_cache' ), 10, 4 ); + } + + /** + * Initialize WPForms compatibility + */ + private function init_wpforms_compat() { + // WPForms AJAX is generally fine, but ensure it's not cached + add_action( 'wp_ajax_wpforms_submit', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_nopriv_wpforms_submit', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_wpforms_file_upload_speed_test', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_nopriv_wpforms_file_upload_speed_test', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_wpforms_restricted_email', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_nopriv_wpforms_restricted_email', array( $this, 'disable_caching' ), 1 ); + } + + /** + * Initialize Wordfence compatibility + */ + private function init_wordfence_compat() { + // Don't interfere with Wordfence login security + add_action( 'wp_ajax_nopriv_wordfence_ls_authenticate', array( $this, 'disable_caching' ), 1 ); + add_action( 'wp_ajax_wordfence_ls_authenticate', array( $this, 'disable_caching' ), 1 ); + + // Don't cache Wordfence blocked pages + if ( defined( 'WORDFENCE_BLOCKED' ) && WORDFENCE_BLOCKED ) { + add_filter( 'maple_performance_should_cache', '__return_false' ); + } + + // Respect Wordfence's caching headers + add_filter( 'maple_performance_should_cache', array( $this, 'check_wordfence_bypass' ) ); + } + + /** + * Initialize Gutenberg FSE / Block Theme compatibility + */ + private function init_gutenberg_fse_compat() { + // Don't aggregate global styles inline CSS + add_filter( 'maple_performance_aggregate_inline_css', '__return_false' ); + + // Protect block editor assets on frontend + add_filter( 'maple_performance_exclude_optimization', array( $this, 'exclude_block_editor_frontend' ) ); + } + + /** + * Exclude block editor frontend from aggressive optimization + */ + public function exclude_block_editor_frontend( $exclude ) { + // If viewing a page with blocks that need JS interaction + // This is conservative - most block content is static + return $exclude; + } + + /** + * Disable caching for current request + */ + public function disable_caching() { + if ( ! defined( 'DONOTCACHEPAGE' ) ) { + define( 'DONOTCACHEPAGE', true ); + } + } + + /** + * Clear product cache + */ + public function clear_product_cache( $product ) { + if ( ! class_exists( 'Maple_Performance_Cache' ) ) { + return; + } + + $product_id = is_numeric( $product ) ? $product : $product->get_id(); + $url = get_permalink( $product_id ); + + if ( $url ) { + $cache = Maple_Performance_Cache::get_instance(); + $cache->clear_url_cache( $url ); + } + + // Also clear shop page + $shop_url = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'shop' ) : ''; + if ( $shop_url ) { + $cache->clear_url_cache( $shop_url ); + } + } + + /** + * Clear cache on order status change + */ + public function clear_on_order_status( $order_id, $old_status, $new_status ) { + // Only clear when status changes might affect stock + $stock_statuses = array( 'completed', 'processing', 'cancelled', 'refunded' ); + + if ( in_array( $new_status, $stock_statuses ) || in_array( $old_status, $stock_statuses ) ) { + $order = wc_get_order( $order_id ); + if ( $order ) { + foreach ( $order->get_items() as $item ) { + $product_id = $item->get_product_id(); + $this->clear_product_cache( $product_id ); + } + } + } + } + + /** + * Exclude WooCommerce pages from JS/CSS optimization + */ + public function exclude_woo_pages( $exclude ) { + if ( function_exists( 'is_cart' ) && is_cart() ) { + return true; + } + if ( function_exists( 'is_checkout' ) && is_checkout() ) { + return true; + } + if ( function_exists( 'is_account_page' ) && is_account_page() ) { + return true; + } + return $exclude; + } + + /** + * Clear LearnDash user-related cache + */ + public function clear_learndash_user_cache( $data ) { + // LearnDash pages are user-specific and already excluded for logged-in users + // But we can clear the course archive pages + if ( ! class_exists( 'Maple_Performance_Cache' ) ) { + return; + } + + $cache = Maple_Performance_Cache::get_instance(); + + // Clear course archive + $course_archive = get_post_type_archive_link( 'sfwd-courses' ); + if ( $course_archive ) { + $cache->clear_url_cache( $course_archive ); + } + + // Clear home page (might show course counts) + $cache->clear_url_cache( home_url( '/' ) ); + } + + /** + * Clear course cache when enrollment changes + */ + public function clear_course_cache( $user_id, $course_id, $access_list, $remove ) { + if ( ! class_exists( 'Maple_Performance_Cache' ) ) { + return; + } + + $cache = Maple_Performance_Cache::get_instance(); + + // Clear the course page (might show enrollment count) + $course_url = get_permalink( $course_id ); + if ( $course_url ) { + $cache->clear_url_cache( $course_url ); + } + } + + /** + * Check Wordfence bypass + */ + public function check_wordfence_bypass( $should_cache ) { + // If Wordfence has set bypass cookie, don't cache + if ( isset( $_COOKIE['wfCBLBypass'] ) ) { + return false; + } + return $should_cache; + } + + /** + * Show admin notices for plugin conflicts + */ + public function conflict_notices() { + // Build list of conflicting caching plugins + $caching_conflicts = $this->get_caching_conflicts(); + + // Show caching conflict warning on all admin pages (dismissible) + if ( ! empty( $caching_conflicts ) && ! get_transient( 'maple_perf_conflict_dismissed' ) ) { + $this->show_conflict_warning( $caching_conflicts ); + } + + // Show detailed info only on Maple Performance settings page + $screen = get_current_screen(); + if ( ! $screen || $screen->id !== 'settings_page_maple-performance' ) { + return; + } + + // If conflicts exist but were dismissed, show a subtle reminder on settings page + if ( ! empty( $caching_conflicts ) && get_transient( 'maple_perf_conflict_dismissed' ) ) { + echo '
'; + echo '

' . esc_html__( 'Reminder:', 'maple-performance' ) . ' '; + echo sprintf( + esc_html__( 'Other caching plugin(s) detected: %s. For best results, use only one caching solution.', 'maple-performance' ), + '' . implode( ', ', array_map( 'esc_html', $caching_conflicts ) ) . '' + ); + echo '

'; + } + + // Show detected compatible plugins (info) + $compatible = array(); + if ( $this->is_detected( 'woocommerce' ) ) $compatible[] = 'WooCommerce'; + if ( $this->is_detected( 'learndash' ) ) $compatible[] = 'LearnDash'; + if ( $this->is_detected( 'wpforms' ) ) $compatible[] = 'WPForms'; + if ( $this->is_detected( 'wordfence' ) ) $compatible[] = 'Wordfence'; + if ( $this->is_detected( 'gravityforms' ) ) $compatible[] = 'Gravity Forms'; + if ( $this->is_detected( 'cf7' ) ) $compatible[] = 'Contact Form 7'; + if ( $this->is_detected( 'elementor' ) ) $compatible[] = 'Elementor'; + + if ( ! empty( $compatible ) ) { + echo '
'; + echo '

' . esc_html__( 'Maple Performance - Detected Plugins:', 'maple-performance' ) . ' '; + echo sprintf( + esc_html__( 'Compatibility rules available for: %s. Enable them in the Plugin Compatibility section below.', 'maple-performance' ), + '' . implode( ', ', array_map( 'esc_html', $compatible ) ) . '' + ); + echo '

'; + } + } + + /** + * Get list of detected caching plugin conflicts + */ + public function get_caching_conflicts() { + $conflicts = array(); + + $caching_plugins = array( + 'wp_rocket' => 'WP Rocket', + 'w3tc' => 'W3 Total Cache', + 'litespeed' => 'LiteSpeed Cache', + 'wp_super_cache' => 'WP Super Cache', + 'wp_fastest_cache' => 'WP Fastest Cache', + 'autoptimize' => 'Autoptimize', + 'cache_enabler' => 'Cache Enabler', + 'hummingbird' => 'Hummingbird', + 'breeze' => 'Breeze', + 'sg_optimizer' => 'SG Optimizer', + 'swift_performance'=> 'Swift Performance', + 'comet_cache' => 'Comet Cache', + 'powered_cache' => 'Powered Cache', + 'perfmatters' => 'Perfmatters', + ); + + foreach ( $caching_plugins as $key => $name ) { + if ( $this->is_detected( $key ) ) { + $conflicts[] = $name; + } + } + + return $conflicts; + } + + /** + * Check if any caching conflicts exist + */ + public function has_caching_conflicts() { + return ! empty( $this->get_caching_conflicts() ); + } + + /** + * Show conflict warning notice + */ + private function show_conflict_warning( $conflicts ) { + // Handle dismiss action + if ( isset( $_GET['maple_dismiss_conflict'] ) && wp_verify_nonce( $_GET['_wpnonce'] ?? '', 'maple_dismiss_conflict' ) ) { + set_transient( 'maple_perf_conflict_dismissed', true, 7 * DAY_IN_SECONDS ); + return; + } + + $dismiss_url = wp_nonce_url( add_query_arg( 'maple_dismiss_conflict', '1' ), 'maple_dismiss_conflict' ); + + echo '
'; + echo '

' . esc_html__( '⚠️ Maple Performance - Caching Conflict Detected', 'maple-performance' ) . '

'; + echo '

' . sprintf( + esc_html__( 'The following caching/optimization plugin(s) are also active: %s', 'maple-performance' ), + '' . implode( ', ', array_map( 'esc_html', $conflicts ) ) . '' + ) . '

'; + echo '

' . esc_html__( 'Running multiple caching plugins simultaneously can cause:', 'maple-performance' ) . '

'; + echo ''; + echo '

' . esc_html__( 'Recommended action:', 'maple-performance' ) . ' '; + echo esc_html__( 'Deactivate the other caching plugin(s), or deactivate Maple Performance if you prefer to keep your existing solution.', 'maple-performance' ); + echo '

'; + echo '

'; + echo '' . esc_html__( 'Go to Plugins', 'maple-performance' ) . ' '; + echo '' . esc_html__( 'Dismiss for 7 days', 'maple-performance' ) . ''; + echo '

'; + echo '
'; + } +} diff --git a/native/wordpress/maple-performance-wp/inc/class-maple-optimize.php b/native/wordpress/maple-performance-wp/inc/class-maple-optimize.php new file mode 100644 index 0000000..d845b2c --- /dev/null +++ b/native/wordpress/maple-performance-wp/inc/class-maple-optimize.php @@ -0,0 +1,1234 @@ +settings = maple_performance()->settings; + $this->init_hooks(); + } + + /** + * Initialize hooks + */ + private function init_hooks() { + // HTML processing - needed for minification, local font display, or iframe lazyload + $needs_html_processing = $this->settings['html_minify'] + || $this->settings['local_font_display'] + || $this->settings['lazyload_iframes']; + + if ( $needs_html_processing ) { + add_action( 'template_redirect', array( $this, 'start_html_buffer' ), -998 ); + } + + // Remove query strings + if ( $this->settings['remove_query_strings'] ) { + add_filter( 'script_loader_src', array( $this, 'remove_query_string' ), 15 ); + add_filter( 'style_loader_src', array( $this, 'remove_query_string' ), 15 ); + } + + // DNS prefetch + if ( $this->settings['dns_prefetch'] ) { + add_action( 'wp_head', array( $this, 'add_dns_prefetch' ), 1 ); + } + + // Preconnect + if ( ! empty( $this->settings['preconnect_domains'] ) ) { + add_action( 'wp_head', array( $this, 'add_preconnect' ), 1 ); + } + + // CSS optimization + if ( $this->settings['css_minify'] || $this->settings['css_aggregate'] ) { + add_action( 'wp_enqueue_scripts', array( $this, 'collect_styles' ), 9999 ); + } + + // JS optimization + if ( $this->settings['js_minify'] || $this->settings['js_aggregate'] ) { + add_action( 'wp_enqueue_scripts', array( $this, 'collect_scripts' ), 9999 ); + } + + // Lazy loading + if ( $this->settings['lazyload_images'] ) { + add_filter( 'the_content', array( $this, 'add_lazyload' ), 99 ); + add_filter( 'post_thumbnail_html', array( $this, 'add_lazyload' ), 99 ); + add_filter( 'widget_text', array( $this, 'add_lazyload' ), 99 ); + } + + // Google Fonts optimization + if ( $this->settings['google_fonts'] !== 'leave' ) { + add_action( 'wp_enqueue_scripts', array( $this, 'process_google_fonts' ), 9999 ); + + // Also process fonts in HTML output for hardcoded fonts + if ( $this->settings['html_minify'] ) { + add_filter( 'maple_performance_html_output', array( $this, 'process_google_fonts_html' ), 10 ); + } + } + + // Local font optimization - add font-display: swap to @font-face rules + if ( $this->settings['local_font_display'] ) { + add_filter( 'maple_performance_html_output', array( $this, 'add_font_display_swap' ), 5 ); + } + + // Font preloading + if ( ! empty( $this->settings['preload_fonts'] ) ) { + add_action( 'wp_head', array( $this, 'output_font_preloads' ), 1 ); + } + } + + /** + * Start HTML buffer for minification + */ + public function start_html_buffer() { + if ( maple_performance()->is_excluded() ) { + return; + } + + ob_start( array( $this, 'process_html' ) ); + } + + /** + * Process HTML output + */ + public function process_html( $html ) { + if ( empty( $html ) ) { + return $html; + } + + // Skip if not HTML + if ( strpos( $html, ' $max_size ) { + return $html; + } + + // Process Google Fonts in HTML (for hardcoded fonts) + $html = apply_filters( 'maple_performance_html_output', $html ); + + // Minify HTML + if ( $this->settings['html_minify'] ) { + $html = $this->minify_html( $html ); + } + + // Add lazy loading to iframes + if ( $this->settings['lazyload_iframes'] ) { + $html = $this->add_iframe_lazyload( $html ); + } + + return $html; + } + + /** + * Minify HTML + */ + private function minify_html( $html ) { + // Additional size check for safety + if ( strlen( $html ) > 2 * 1024 * 1024 ) { + return $html; + } + + // Preserve pre, script, style, textarea content + $preserve = array(); + $preserve_tags = array( 'pre', 'script', 'style', 'textarea', 'svg' ); + + foreach ( $preserve_tags as $tag ) { + // Use atomic grouping concept with possessive quantifier simulation + // Limit matches to prevent catastrophic backtracking + $pattern = '/<' . $tag . '[^>]*>.*?<\/' . $tag . '>/is'; + + // Set a reasonable limit on preg operations + $match_count = preg_match_all( $pattern, $html, $matches ); + + // Limit to 500 preserved blocks per tag type to prevent memory issues + if ( $match_count > 500 ) { + $matches[0] = array_slice( $matches[0], 0, 500 ); + } + + foreach ( $matches[0] as $i => $match ) { + $placeholder = ''; + $preserve[ $placeholder ] = $match; + $html = str_replace( $match, $placeholder, $html ); + } + } + + // Remove HTML comments (except IE conditionals and preserved) + if ( $this->settings['html_remove_comments'] ) { + $html = preg_replace( '//s', '', $html ); + } + + // Remove whitespace between tags + $html = preg_replace( '/>\s+<', $html ); + + // Collapse multiple spaces + $html = preg_replace( '/\s{2,}/', ' ', $html ); + + // Remove spaces around attributes + $html = preg_replace( '/\s+([a-zA-Z-]+)=/', ' $1=', $html ); + + // Restore preserved content + $html = str_replace( array_keys( $preserve ), array_values( $preserve ), $html ); + + return trim( $html ); + } + + /** + * Remove query strings from static resources + */ + public function remove_query_string( $src ) { + if ( strpos( $src, '?ver=' ) !== false ) { + $src = remove_query_arg( 'ver', $src ); + } + return $src; + } + + /** + * Add DNS prefetch hints + */ + public function add_dns_prefetch() { + $domains = array( + 'fonts.googleapis.com', + 'fonts.gstatic.com', + ); + + // Add from preconnect settings + if ( ! empty( $this->settings['preconnect_domains'] ) ) { + foreach ( $this->settings['preconnect_domains'] as $domain ) { + $parsed = parse_url( $domain ); + if ( ! empty( $parsed['host'] ) ) { + $domains[] = $parsed['host']; + } + } + } + + $domains = array_unique( $domains ); + + foreach ( $domains as $domain ) { + echo '' . "\n"; + } + } + + /** + * Add preconnect hints + */ + public function add_preconnect() { + foreach ( $this->settings['preconnect_domains'] as $domain ) { + if ( ! empty( $domain ) ) { + // Ensure URL has scheme + if ( strpos( $domain, '//' ) === false ) { + $domain = 'https://' . $domain; + } + echo '' . "\n"; + } + } + } + + /** + * Collect enqueued styles + */ + public function collect_styles() { + if ( ! $this->settings['css_aggregate'] ) { + return; + } + + global $wp_styles; + + if ( empty( $wp_styles->queue ) ) { + return; + } + + $to_aggregate = array(); + + foreach ( $wp_styles->queue as $handle ) { + // Skip if excluded + if ( $this->is_excluded_css( $handle ) ) { + continue; + } + + $style = $wp_styles->registered[ $handle ] ?? null; + if ( ! $style || empty( $style->src ) ) { + continue; + } + + // Only aggregate local files + $src = $style->src; + if ( $this->is_external_url( $src ) ) { + continue; + } + + $to_aggregate[] = array( + 'handle' => $handle, + 'src' => $src, + 'deps' => $style->deps, + 'ver' => $style->ver, + 'media' => $style->args ?? 'all', + ); + + // Dequeue original + wp_dequeue_style( $handle ); + } + + if ( ! empty( $to_aggregate ) ) { + $this->create_aggregated_css( $to_aggregate ); + } + } + + /** + * Check if style should be excluded + */ + private function is_excluded_css( $handle ) { + $exclude = array_merge( + array( 'admin-bar', 'dashicons' ), + $this->settings['exclude_css'] + ); + + // Apply compat filter for detected plugins + $exclude = apply_filters( 'maple_performance_css_exclusions', $exclude ); + + foreach ( $exclude as $pattern ) { + if ( ! empty( $pattern ) && strpos( $handle, $pattern ) !== false ) { + return true; + } + } + + return false; + } + + /** + * Create aggregated CSS file + */ + private function create_aggregated_css( $styles ) { + $combined = ''; + $total_size = 0; + $max_total_size = 2 * 1024 * 1024; // 2MB max combined size + $max_file_size = 500 * 1024; // 500KB max per file + $max_files = 50; // Max files to aggregate + $file_count = 0; + + foreach ( $styles as $style ) { + // Limit number of files to aggregate + if ( ++$file_count > $max_files ) { + break; + } + + $file_path = $this->url_to_path( $style['src'] ); + + if ( ! $file_path || ! file_exists( $file_path ) ) { + continue; + } + + // Check file size before reading + $file_size = filesize( $file_path ); + if ( $file_size > $max_file_size ) { + continue; // Skip files that are too large + } + + // Check if adding this file would exceed total limit + if ( $total_size + $file_size > $max_total_size ) { + break; + } + + $content = file_get_contents( $file_path ); + if ( false === $content ) { + continue; + } + + $total_size += strlen( $content ); + + // Fix relative URLs in CSS + $content = $this->fix_css_urls( $content, dirname( $style['src'] ) ); + + // Minify if enabled + if ( $this->settings['css_minify'] ) { + $content = $this->minify_css( $content ); + } + + $combined .= "/* {$style['handle']} */\n" . $content . "\n"; + } + + if ( empty( $combined ) ) { + return; + } + + // Generate hash for filename + $hash = substr( md5( $combined ), 0, 12 ); + $filename = 'maple-css-' . $hash . '.css'; + $filepath = MAPLE_PERF_CACHE_DIR . 'assets/' . $filename; + $fileurl = MAPLE_PERF_CACHE_URL . 'assets/' . $filename; + + // Verify filepath is within allowed directory + $assets_dir = realpath( MAPLE_PERF_CACHE_DIR . 'assets/' ); + if ( false === $assets_dir ) { + // Assets directory doesn't exist yet, create it + wp_mkdir_p( MAPLE_PERF_CACHE_DIR . 'assets/' ); + $assets_dir = realpath( MAPLE_PERF_CACHE_DIR . 'assets/' ); + } + + if ( false === $assets_dir ) { + return; + } + + // Write file if doesn't exist + if ( ! file_exists( $filepath ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $filepath, $combined ); + if ( file_exists( $filepath ) ) { + chmod( $filepath, 0644 ); + } + + // Create gzipped version + if ( function_exists( 'gzencode' ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $filepath . '.gz', gzencode( $combined, 9 ) ); + if ( file_exists( $filepath . '.gz' ) ) { + chmod( $filepath . '.gz', 0644 ); + } + } + } + + // Enqueue aggregated file + wp_enqueue_style( 'maple-aggregated-css', $fileurl, array(), null, 'all' ); + + // Add defer if enabled + if ( $this->settings['css_defer'] ) { + add_filter( 'style_loader_tag', array( $this, 'defer_css' ), 10, 2 ); + } + } + + /** + * Defer CSS loading + */ + public function defer_css( $tag, $handle ) { + if ( $handle !== 'maple-aggregated-css' ) { + return $tag; + } + + // Convert to preload + onload pattern + $tag = str_replace( + "rel='stylesheet'", + "rel='preload' as='style' onload=\"this.onload=null;this.rel='stylesheet'\"", + $tag + ); + + // Add noscript fallback + $noscript = ''; + + return $tag . $noscript; + } + + /** + * Fix relative URLs in CSS + */ + private function fix_css_urls( $css, $base_url ) { + // Ensure base URL has trailing slash + $base_url = trailingslashit( $base_url ); + + // Fix url() references + $css = preg_replace_callback( + '/url\s*\(\s*[\'"]?\s*(?!data:|https?:|\/\/)(.*?)\s*[\'"]?\s*\)/i', + function( $matches ) use ( $base_url ) { + $url = $matches[1]; + + // Handle relative paths + if ( strpos( $url, '../' ) === 0 ) { + // Go up directories + $parts = explode( '../', $url ); + $up_count = count( $parts ) - 1; + $base_parts = explode( '/', rtrim( $base_url, '/' ) ); + + for ( $i = 0; $i < $up_count; $i++ ) { + array_pop( $base_parts ); + } + + $new_base = implode( '/', $base_parts ) . '/'; + $url = $new_base . end( $parts ); + } else { + $url = $base_url . ltrim( $url, './' ); + } + + return 'url(' . $url . ')'; + }, + $css + ); + + return $css; + } + + /** + * Minify CSS + */ + private function minify_css( $css ) { + // Skip minification for very large CSS files to prevent performance issues + // 500KB is a reasonable limit for inline minification + if ( strlen( $css ) > 500 * 1024 ) { + return $css; + } + + // Remove comments - use simpler pattern to avoid catastrophic backtracking + $css = preg_replace( '#/\*[^*]*\*+(?:[^/*][^*]*\*+)*/#', '', $css ); + + // Remove whitespace + $css = preg_replace( '/\s+/', ' ', $css ); + + // Remove spaces around special characters + $css = preg_replace( '/\s*([\{\}\:\;\,])\s*/', '$1', $css ); + + // Remove trailing semicolons before closing braces + $css = str_replace( ';}', '}', $css ); + + // Remove empty rules - simplified pattern + $css = preg_replace( '/[^\{\}]+\{\}/', '', $css ); + + return trim( $css ); + } + + /** + * Collect enqueued scripts + */ + public function collect_scripts() { + if ( ! $this->settings['js_aggregate'] ) { + // Just minify individual scripts if enabled + if ( $this->settings['js_minify'] ) { + add_filter( 'script_loader_tag', array( $this, 'maybe_minify_script' ), 10, 3 ); + } + return; + } + + global $wp_scripts; + + if ( empty( $wp_scripts->queue ) ) { + return; + } + + $to_aggregate = array(); + + foreach ( $wp_scripts->queue as $handle ) { + // Skip if excluded + if ( $this->is_excluded_js( $handle ) ) { + continue; + } + + $script = $wp_scripts->registered[ $handle ] ?? null; + if ( ! $script || empty( $script->src ) ) { + continue; + } + + // Only aggregate local files + $src = $script->src; + if ( $this->is_external_url( $src ) ) { + continue; + } + + $to_aggregate[] = array( + 'handle' => $handle, + 'src' => $src, + 'deps' => $script->deps, + 'ver' => $script->ver, + ); + + // Dequeue original + wp_dequeue_script( $handle ); + } + + if ( ! empty( $to_aggregate ) ) { + $this->create_aggregated_js( $to_aggregate ); + } + } + + /** + * Check if script should be excluded + */ + private function is_excluded_js( $handle ) { + $exclude = $this->settings['exclude_js']; + + // Always exclude jQuery if setting enabled + if ( $this->settings['js_exclude_jquery'] ) { + $exclude = array_merge( $exclude, array( 'jquery', 'jquery-core', 'jquery-migrate' ) ); + } + + // Add admin/core scripts + $exclude = array_merge( $exclude, array( 'admin-bar', 'wp-embed' ) ); + + // Apply compat filter for detected plugins + $exclude = apply_filters( 'maple_performance_js_exclusions', $exclude ); + + foreach ( $exclude as $pattern ) { + if ( ! empty( $pattern ) && strpos( $handle, $pattern ) !== false ) { + return true; + } + } + + return false; + } + + /** + * Create aggregated JS file + */ + private function create_aggregated_js( $scripts ) { + $combined = ''; + $total_size = 0; + $max_total_size = 2 * 1024 * 1024; // 2MB max combined size + $max_file_size = 500 * 1024; // 500KB max per file + $max_files = 50; // Max files to aggregate + $file_count = 0; + + foreach ( $scripts as $script ) { + // Limit number of files to aggregate + if ( ++$file_count > $max_files ) { + break; + } + + $file_path = $this->url_to_path( $script['src'] ); + + if ( ! $file_path || ! file_exists( $file_path ) ) { + continue; + } + + // Check file size before reading + $file_size = filesize( $file_path ); + if ( $file_size > $max_file_size ) { + continue; // Skip files that are too large + } + + // Check if adding this file would exceed total limit + if ( $total_size + $file_size > $max_total_size ) { + break; + } + + $content = file_get_contents( $file_path ); + if ( false === $content ) { + continue; + } + + $total_size += strlen( $content ); + + // Minify if enabled + if ( $this->settings['js_minify'] ) { + $content = $this->minify_js( $content ); + } + + // Ensure semicolon at end + $content = rtrim( $content, "; \n\r" ) . ";\n"; + + $combined .= "/* {$script['handle']} */\n" . $content . "\n"; + } + + if ( empty( $combined ) ) { + return; + } + + // Generate hash for filename + $hash = substr( md5( $combined ), 0, 12 ); + $filename = 'maple-js-' . $hash . '.js'; + $filepath = MAPLE_PERF_CACHE_DIR . 'assets/' . $filename; + $fileurl = MAPLE_PERF_CACHE_URL . 'assets/' . $filename; + + // Verify filepath is within allowed directory + $assets_dir = realpath( MAPLE_PERF_CACHE_DIR . 'assets/' ); + if ( false === $assets_dir ) { + // Assets directory doesn't exist yet, create it + wp_mkdir_p( MAPLE_PERF_CACHE_DIR . 'assets/' ); + $assets_dir = realpath( MAPLE_PERF_CACHE_DIR . 'assets/' ); + } + + if ( false === $assets_dir ) { + return; + } + + // Write file if doesn't exist + if ( ! file_exists( $filepath ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $filepath, $combined ); + if ( file_exists( $filepath ) ) { + chmod( $filepath, 0644 ); + } + + // Create gzipped version + if ( function_exists( 'gzencode' ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $filepath . '.gz', gzencode( $combined, 9 ) ); + if ( file_exists( $filepath . '.gz' ) ) { + chmod( $filepath . '.gz', 0644 ); + } + } + } + + // Enqueue aggregated file in footer + wp_enqueue_script( 'maple-aggregated-js', $fileurl, array( 'jquery' ), null, true ); + + // Add defer if enabled + if ( $this->settings['js_defer'] ) { + add_filter( 'script_loader_tag', array( $this, 'defer_js' ), 10, 2 ); + } + } + + /** + * Defer JS loading + */ + public function defer_js( $tag, $handle ) { + if ( $handle !== 'maple-aggregated-js' ) { + return $tag; + } + + return str_replace( ' src', ' defer src', $tag ); + } + + /** + * Basic JS minification + */ + private function minify_js( $js ) { + // Skip minification for very large JS files to prevent performance issues + // 500KB is a reasonable limit for inline minification + if ( strlen( $js ) > 500 * 1024 ) { + return $js; + } + + // Remove single-line comments (but not URLs) + $js = preg_replace( '#(?is_excluded_js( $handle ) ) { + return $tag; + } + + // Skip external scripts + if ( $this->is_external_url( $src ) ) { + return $tag; + } + + return $tag; + } + + /** + * Add lazy loading to images + */ + public function add_lazyload( $content ) { + if ( empty( $content ) ) { + return $content; + } + + // Don't lazyload in admin + if ( is_admin() ) { + return $content; + } + + // Find all img tags + $content = preg_replace_callback( + '/]+)>/i', + array( $this, 'process_image_tag' ), + $content + ); + + return $content; + } + + /** + * Process individual image tag for lazy loading + */ + private function process_image_tag( $matches ) { + $img = $matches[0]; + $attrs = $matches[1]; + + // Skip if already has loading attribute + if ( strpos( $attrs, 'loading=' ) !== false ) { + return $img; + } + + // Skip if has fetchpriority="high" (LCP image) + if ( strpos( $attrs, 'fetchpriority="high"' ) !== false ) { + return $img; + } + + // Check exclusions + foreach ( $this->settings['lazyload_exclude'] as $exclude ) { + if ( ! empty( $exclude ) && strpos( $attrs, $exclude ) !== false ) { + return $img; + } + } + + // Add loading="lazy" + return str_replace( ']+)>/i', + function( $matches ) { + $iframe = $matches[0]; + $attrs = $matches[1]; + + // Skip if already has loading attribute + if ( strpos( $attrs, 'loading=' ) !== false ) { + return $iframe; + } + + return str_replace( 'queue ) ) { + return; + } + + $google_fonts = array(); + $handles_to_remove = array(); + + // Find all Google Fonts + foreach ( $wp_styles->queue as $handle ) { + $style = $wp_styles->registered[ $handle ] ?? null; + if ( ! $style || empty( $style->src ) ) { + continue; + } + + if ( $this->is_google_font_url( $style->src ) ) { + $google_fonts[] = $style->src; + $handles_to_remove[] = $handle; + } + } + + if ( empty( $google_fonts ) ) { + return; + } + + // Handle based on setting + switch ( $this->settings['google_fonts'] ) { + case 'remove': + // Just dequeue them + foreach ( $handles_to_remove as $handle ) { + wp_dequeue_style( $handle ); + } + break; + + case 'combine': + case 'defer': + // Dequeue originals + foreach ( $handles_to_remove as $handle ) { + wp_dequeue_style( $handle ); + } + + // Combine and re-enqueue + $combined_url = $this->combine_google_fonts( $google_fonts ); + + if ( $combined_url ) { + if ( $this->settings['google_fonts'] === 'defer' ) { + // Add via wp_head with preload/swap pattern + add_action( 'wp_head', function() use ( $combined_url ) { + $this->output_deferred_google_fonts( $combined_url ); + }, 5 ); + } else { + // Standard enqueue with display=swap + wp_enqueue_style( 'maple-google-fonts', $combined_url, array(), null ); + } + } + break; + } + } + + /** + * Check if URL is a Google Fonts URL + */ + private function is_google_font_url( $url ) { + return strpos( $url, 'fonts.googleapis.com' ) !== false + || strpos( $url, 'fonts.gstatic.com' ) !== false; + } + + /** + * Combine multiple Google Fonts URLs into one + */ + private function combine_google_fonts( $urls ) { + $families = array(); + + foreach ( $urls as $url ) { + // Parse the URL + $parsed = parse_url( $url ); + if ( empty( $parsed['query'] ) ) { + continue; + } + + parse_str( $parsed['query'], $params ); + + // Handle both 'family' parameter formats + if ( ! empty( $params['family'] ) ) { + // Can be single family or multiple separated by | + $font_families = explode( '|', $params['family'] ); + foreach ( $font_families as $family ) { + $families[] = trim( $family ); + } + } + } + + if ( empty( $families ) ) { + return false; + } + + // Remove duplicates + $families = array_unique( $families ); + + // Build combined URL with display=swap + $combined_url = 'https://fonts.googleapis.com/css?family=' . implode( '|', array_map( 'urlencode', $families ) ) . '&display=swap'; + + // Check if using CSS2 format (newer) + $has_css2 = false; + foreach ( $urls as $url ) { + if ( strpos( $url, 'fonts.googleapis.com/css2' ) !== false ) { + $has_css2 = true; + break; + } + } + + // If any URL uses CSS2, convert to CSS2 format + if ( $has_css2 ) { + $combined_url = $this->convert_to_css2_format( $families ); + } + + return $combined_url; + } + + /** + * Convert font families to CSS2 format + */ + private function convert_to_css2_format( $families ) { + $family_params = array(); + + foreach ( $families as $family ) { + // Parse weight/style from family string + // Format: "Roboto:400,700" or "Roboto:wght@400;700" + if ( strpos( $family, ':' ) !== false ) { + list( $name, $weights ) = explode( ':', $family, 2 ); + + // Convert old format weights to CSS2 format + $weights = str_replace( ',', ';', $weights ); + + // Check if already in wght@ format + if ( strpos( $weights, 'wght@' ) === false && strpos( $weights, 'ital@' ) === false ) { + $weights = 'wght@' . $weights; + } + + $family_params[] = 'family=' . urlencode( $name ) . ':' . $weights; + } else { + $family_params[] = 'family=' . urlencode( $family ); + } + } + + return 'https://fonts.googleapis.com/css2?' . implode( '&', $family_params ) . '&display=swap'; + } + + /** + * Output deferred Google Fonts (non-render-blocking) + */ + private function output_deferred_google_fonts( $url ) { + // Preconnect to Google Fonts domains + echo '' . "\n"; + echo '' . "\n"; + + // Preload with swap to deferred stylesheet + echo '' . "\n"; + + // Noscript fallback + echo '' . "\n"; + } + + /** + * Process Google Fonts in HTML output (for hardcoded fonts) + */ + public function process_google_fonts_html( $html ) { + if ( $this->settings['google_fonts'] === 'leave' ) { + return $html; + } + + // Find all Google Fonts links in HTML + $pattern = '/]+href=[\'"]([^\'"]*fonts\.googleapis\.com[^\'"]*)[\'"][^>]*>/i'; + + if ( ! preg_match_all( $pattern, $html, $matches, PREG_SET_ORDER ) ) { + return $html; + } + + // Limit to 20 font URLs to prevent memory issues + $matches = array_slice( $matches, 0, 20 ); + + $google_font_urls = array(); + $tags_to_remove = array(); + + foreach ( $matches as $match ) { + $tags_to_remove[] = $match[0]; + $google_font_urls[] = $match[1]; + } + + // Remove original tags + foreach ( $tags_to_remove as $tag ) { + $html = str_replace( $tag, '', $html ); + } + + if ( $this->settings['google_fonts'] === 'remove' ) { + return $html; + } + + // Combine fonts + $combined_url = $this->combine_google_fonts( $google_font_urls ); + + if ( ! $combined_url ) { + return $html; + } + + // Create replacement tag + if ( $this->settings['google_fonts'] === 'defer' ) { + $replacement = '' . "\n"; + $replacement .= '' . "\n"; + $replacement .= '' . "\n"; + $replacement .= '' . "\n"; + } else { + $replacement = '' . "\n"; + } + + // Insert after + $html = preg_replace( '/]*)>/i', '' . "\n" . $replacement, $html, 1 ); + + return $html; + } + + /** + * Add font-display: swap to all @font-face rules in inline styles + * This fixes the "Ensure text remains visible during webfont load" issue for local fonts + */ + public function add_font_display_swap( $html ) { + if ( empty( $html ) ) { + return $html; + } + + // Find all