OptimizerWebPageCache.php
7 months ago
OptimizerWebPageCacheWP.php
7 months ago
OptimizerWebPageValidations.php
1 year ago
advanced-cache.php
3 years ago
OptimizerWebPageCacheWP.php
454 lines
| 1 | <?php |
| 2 | |
| 3 | namespace TenWebOptimizer\WebPageCache; |
| 4 | |
| 5 | use TenWebOptimizer\OptimizerUtils; |
| 6 | |
| 7 | /* |
| 8 | * Base class other (more-specific) classes inherit from. |
| 9 | */ |
| 10 | if (!defined('ABSPATH')) { |
| 11 | exit; |
| 12 | } |
| 13 | |
| 14 | /** |
| 15 | * This class will work with wp hooks/filter. Will decide when to clear cache, enable/disable web caching, etc. |
| 16 | * |
| 17 | * */ |
| 18 | class OptimizerWebPageCacheWP |
| 19 | { |
| 20 | protected static $instance = null; |
| 21 | |
| 22 | public $wp_config_file_path = ABSPATH . '/wp-config.php'; |
| 23 | |
| 24 | public $page_cache_config_dir = WP_CONTENT_DIR . '/10web-page-cache-config'; |
| 25 | |
| 26 | public $wp_cache_define = "define( 'WP_CACHE', true );\ndefine( 'TWO_PLUGIN_DIR_CACHE', '" . TENWEB_SO_PLUGIN_DIR . "' );"; |
| 27 | |
| 28 | public $advanced_cache_path = WP_CONTENT_DIR . '/advanced-cache.php'; |
| 29 | |
| 30 | public function __construct() |
| 31 | { |
| 32 | if (isset($_GET['action']) && $_GET['action'] === 'two_clear_page_cache') { // phpcs:ignore |
| 33 | add_action('admin_init', [ $this, 'maybe_clear_page_cache' ]); |
| 34 | } |
| 35 | |
| 36 | global $TwoSettings; |
| 37 | |
| 38 | if ($TwoSettings->get_settings('two_page_cache') === 'on' && get_option('show_on_front') === 'posts') { |
| 39 | add_action('transition_post_status', [$this, 'transition_post_status'], 10, 3); |
| 40 | } |
| 41 | |
| 42 | if ((!isset($_GET['action']) || $_GET['action'] != 'deactivate') // phpcs:ignore |
| 43 | && (!isset($_GET['rest_route']) || $_GET['rest_route'] != '/tenweb_so/v1/set_modes')) { // phpcs:ignore |
| 44 | add_action('update_option_two_settings', [ $this, 'enable_disable_page_cache' ], 10, 3); |
| 45 | } |
| 46 | |
| 47 | add_filter('page_row_actions', [$this, 'post_row_actions'], 10, 2); |
| 48 | add_filter('post_row_actions', [$this, 'post_row_actions'], 10, 2); |
| 49 | |
| 50 | if ($TwoSettings->get_settings('two_page_cache') === 'on' && $TwoSettings->get_settings('two_page_cache_user') === 'on') { |
| 51 | if (is_user_logged_in()) { |
| 52 | // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPress.Security.ValidatedSanitizedInput.InputNotValidated |
| 53 | $logged_in_cookie = sanitize_text_field($_COOKIE[LOGGED_IN_COOKIE]); |
| 54 | // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie |
| 55 | setcookie('tenweb_so_page_cache_hash', md5($logged_in_cookie), time() + (int) $TwoSettings->get_settings('two_page_cache_life_time'), COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); |
| 56 | } else { |
| 57 | if (isset($_COOKIE['tenweb_so_page_cache_hash'])) { |
| 58 | // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie |
| 59 | setcookie('tenweb_so_page_cache_hash', '', time() + (int) $TwoSettings->get_settings('two_page_cache_life_time'), COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true); |
| 60 | } |
| 61 | } |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | public function delete_all_cache() |
| 66 | { |
| 67 | OptimizerWebPageCache::delete_all_cached_pages(); |
| 68 | } |
| 69 | |
| 70 | public function post_updated($post_id, $post_after, $post_before) |
| 71 | { |
| 72 | OptimizerWebPageCache::delete_cache_by_post_id($post_id); |
| 73 | } |
| 74 | |
| 75 | public function enable_disable_page_cache($old_value, $value, $option) |
| 76 | { |
| 77 | $value = json_decode($value, true); |
| 78 | $value = (isset($value[ 'two_page_cache' ])) ? $value['two_page_cache'] : ''; |
| 79 | |
| 80 | if ($value === 'on') { |
| 81 | $this->store_page_cache_configs(); |
| 82 | |
| 83 | if (!file_exists(WP_CONTENT_DIR . '/advanced-cache.php')) { |
| 84 | $this->enable_page_cache(); |
| 85 | } else { |
| 86 | $file_content = file_get_contents(WP_CONTENT_DIR . '/advanced-cache.php'); |
| 87 | |
| 88 | if (strpos($file_content, 'TENWEB_SO_ADVANCED_CACHE') === false) { |
| 89 | $this->enable_page_cache(); |
| 90 | } elseif (!defined('WP_CACHE') || !WP_CACHE || !defined('TWO_PLUGIN_DIR_CACHE')) { |
| 91 | $this->add_wp_cache_constant(); |
| 92 | } |
| 93 | } |
| 94 | } else { |
| 95 | $this->disable_page_cache(); |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | public function enable_page_cache() |
| 100 | { |
| 101 | if ($this->add_wp_cache_constant() === false) { |
| 102 | return false; |
| 103 | } |
| 104 | |
| 105 | return copy(TENWEB_SO_PLUGIN_DIR . 'includes/WebPageCache/advanced-cache.php', $this->advanced_cache_path); |
| 106 | } |
| 107 | |
| 108 | public function disable_page_cache() |
| 109 | { |
| 110 | if ($this->remove_wp_cache_constant() === false) { |
| 111 | return false; |
| 112 | } |
| 113 | |
| 114 | if ($this->delete_page_cache_configs() === false) { |
| 115 | return false; |
| 116 | } |
| 117 | |
| 118 | if (!file_exists($this->advanced_cache_path)) { |
| 119 | return true; |
| 120 | } |
| 121 | |
| 122 | return unlink($this->advanced_cache_path); // phpcs:ignore |
| 123 | } |
| 124 | |
| 125 | public function add_wp_cache_constant() |
| 126 | { |
| 127 | // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_is_writable |
| 128 | if (!is_writable($this->wp_config_file_path) || !is_readable($this->wp_config_file_path)) { |
| 129 | return false; |
| 130 | } |
| 131 | |
| 132 | // Save untouched version of the file |
| 133 | $original_file_content = file_get_contents($this->wp_config_file_path); // phpcs:ignore |
| 134 | |
| 135 | if ($original_file_content === false) { |
| 136 | error_log('10Web Speed Optimizer: Failed to read original wp-config.php file'); // phpcs:ignore |
| 137 | |
| 138 | return false; |
| 139 | } |
| 140 | |
| 141 | $file_content = $original_file_content; |
| 142 | |
| 143 | // Remove our constants to clear the file in case if somebody removed the WP_CACHE define |
| 144 | $file_content = preg_replace('/# BEGIN WP Cache by 10Web[\s\S]*# END WP Cache by 10Web/', '', $file_content); |
| 145 | |
| 146 | if ($file_content === null) { |
| 147 | error_log('10Web Speed Optimizer: Failed to remove existing WP Cache constants'); // phpcs:ignore |
| 148 | |
| 149 | return false; |
| 150 | } |
| 151 | |
| 152 | // Remove WP_CACHE defined by others |
| 153 | $file_content = OptimizerUtils::delete_define('WP_CACHE', $file_content); |
| 154 | |
| 155 | if (strpos($file_content, '<?php') === false) { |
| 156 | // Restore original file content |
| 157 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 158 | error_log('10Web Speed Optimizer: Failed to remove existing WP_CACHE define'); // phpcs:ignore |
| 159 | |
| 160 | return false; |
| 161 | } |
| 162 | |
| 163 | // Add new defines |
| 164 | $replacement = "<?php\n# BEGIN WP Cache by 10Web\n" . $this->wp_cache_define . "\n# END WP Cache by 10Web\n"; |
| 165 | $file_content = preg_replace('@<\?php\s*@i', $replacement, $file_content, 1); |
| 166 | |
| 167 | if ($file_content === null) { |
| 168 | // Restore original file content |
| 169 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 170 | error_log('10Web Speed Optimizer: Failed to add new WP Cache constants'); // phpcs:ignore |
| 171 | |
| 172 | return false; |
| 173 | } |
| 174 | |
| 175 | // Ensure essential WordPress constants are still present |
| 176 | if (strpos($file_content, 'require_once') === false || |
| 177 | strpos($file_content, 'ABSPATH') === false) { |
| 178 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 179 | error_log('10Web Speed Optimizer: Essential WordPress constants missing after modification'); // phpcs:ignore |
| 180 | |
| 181 | return false; |
| 182 | } |
| 183 | |
| 184 | // Write to temporary file first for atomic operation |
| 185 | $temp_file = $this->wp_config_file_path . '.tmp.' . uniqid(); |
| 186 | $write_result = file_put_contents($temp_file, $file_content); // phpcs:ignore |
| 187 | |
| 188 | if ($write_result === false) { |
| 189 | // Clean up temp file and restore original content |
| 190 | if (file_exists($temp_file)) { |
| 191 | unlink($temp_file); // phpcs:ignore |
| 192 | } |
| 193 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 194 | error_log('10Web Speed Optimizer: Failed to write modified content to temporary file'); // phpcs:ignore |
| 195 | |
| 196 | return false; |
| 197 | } |
| 198 | |
| 199 | // Atomic rename operation |
| 200 | $rename_result = rename($temp_file, $this->wp_config_file_path); // phpcs:ignore |
| 201 | |
| 202 | if (!$rename_result) { |
| 203 | // Clean up temp file and restore original content |
| 204 | unlink($temp_file); // phpcs:ignore |
| 205 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 206 | error_log('10Web Speed Optimizer: Failed to rename temporary file to wp-config.php'); // phpcs:ignore |
| 207 | |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | return true; |
| 212 | } |
| 213 | |
| 214 | public function remove_wp_cache_constant() |
| 215 | { |
| 216 | // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_is_writable |
| 217 | if (!is_writable($this->wp_config_file_path) || !is_readable($this->wp_config_file_path)) { |
| 218 | return false; |
| 219 | } |
| 220 | |
| 221 | // Save untouched version of the file |
| 222 | $original_file_content = file_get_contents($this->wp_config_file_path); // phpcs:ignore |
| 223 | |
| 224 | if ($original_file_content === false) { |
| 225 | error_log('10Web Speed Optimizer: Failed to read original wp-config.php file'); // phpcs:ignore |
| 226 | |
| 227 | return false; |
| 228 | } |
| 229 | |
| 230 | $file_content = $original_file_content; |
| 231 | $file_content = preg_replace('/# BEGIN WP Cache by 10Web[\s\S]*# END WP Cache by 10Web/', '', $file_content); |
| 232 | |
| 233 | if ($file_content === null) { |
| 234 | error_log('10Web Speed Optimizer: Failed to remove WP Cache constants'); // phpcs:ignore |
| 235 | |
| 236 | return false; |
| 237 | } |
| 238 | |
| 239 | // Write to temporary file first for atomic operation |
| 240 | $temp_file = $this->wp_config_file_path . '.tmp.' . uniqid(); |
| 241 | $write_result = file_put_contents($temp_file, $file_content); // phpcs:ignore |
| 242 | |
| 243 | if ($write_result === false) { |
| 244 | // Clean up temp file and restore original content |
| 245 | if (file_exists($temp_file)) { |
| 246 | unlink($temp_file); // phpcs:ignore |
| 247 | } |
| 248 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 249 | error_log('10Web Speed Optimizer: Failed to write modified content to temporary file'); // phpcs:ignore |
| 250 | |
| 251 | return false; |
| 252 | } |
| 253 | |
| 254 | // Atomic rename operation |
| 255 | $rename_result = rename($temp_file, $this->wp_config_file_path); // phpcs:ignore |
| 256 | |
| 257 | if (!$rename_result) { |
| 258 | // Clean up temp file and restore original content |
| 259 | unlink($temp_file); // phpcs:ignore |
| 260 | file_put_contents($this->wp_config_file_path, $original_file_content); // phpcs:ignore |
| 261 | error_log('10Web Speed Optimizer: Failed to rename temporary file to wp-config.php'); // phpcs:ignore |
| 262 | |
| 263 | return false; |
| 264 | } |
| 265 | |
| 266 | return true; |
| 267 | } |
| 268 | |
| 269 | public function store_page_cache_configs() |
| 270 | { |
| 271 | if (!is_dir($this->page_cache_config_dir) && !mkdir($concurrentDirectory = $this->page_cache_config_dir, 0777) && !is_dir($concurrentDirectory)) { // phpcs:ignore |
| 272 | return false; |
| 273 | } |
| 274 | $configs = []; |
| 275 | $TwoSettings = new \TenWebOptimizer\OptimizerSettings(); |
| 276 | $configs['two_settings'] = json_encode($TwoSettings->get_settings()); // phpcs:ignore |
| 277 | |
| 278 | return (bool) file_put_contents($this->page_cache_config_dir . '/config.json', json_encode($configs)); // phpcs:ignore |
| 279 | } |
| 280 | |
| 281 | public function delete_page_cache_configs() |
| 282 | { |
| 283 | if (file_exists($this->page_cache_config_dir . '/config.json')) { |
| 284 | return unlink($this->page_cache_config_dir . '/config.json'); // phpcs:ignore |
| 285 | } |
| 286 | |
| 287 | return true; |
| 288 | } |
| 289 | |
| 290 | public function maybe_clear_page_cache() |
| 291 | { |
| 292 | // Check user capabilities |
| 293 | if (!current_user_can('manage_options')) { |
| 294 | wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'tenweb-speed-optimizer'), 403); |
| 295 | } |
| 296 | |
| 297 | // Verify nonce |
| 298 | $nonce = isset($_GET['_wpnonce']) ? sanitize_text_field($_GET['_wpnonce']) : ''; |
| 299 | |
| 300 | if (!wp_verify_nonce($nonce, 'two_clear_page_cache')) { |
| 301 | wp_die(esc_html__('Security check failed. Please try again.', 'tenweb-speed-optimizer'), 403); |
| 302 | } |
| 303 | |
| 304 | if (!empty($_GET['permalink'])) { |
| 305 | $permalink = sanitize_url($_GET['permalink']); // phpcs:ignore |
| 306 | |
| 307 | // Validate URL - reject if contains dangerous characters instead of trying to fix it |
| 308 | if (!self::is_url_safe_for_cache_clear($permalink)) { |
| 309 | status_header(400); |
| 310 | wp_die(esc_html__('Invalid URL: contains dangerous characters.', 'tenweb-speed-optimizer'), 400); |
| 311 | } |
| 312 | |
| 313 | // Validate that the cache directory path stays within allowed boundaries |
| 314 | $cache_dir = OptimizerWebPageCache::get_cache_dir_for_page_from_url($permalink); |
| 315 | $cache_dir = self::validate_cache_path($cache_dir); |
| 316 | |
| 317 | if ($cache_dir === false) { |
| 318 | wp_die(esc_html__('Invalid cache path detected.', 'tenweb-speed-optimizer'), 403); |
| 319 | } |
| 320 | |
| 321 | OptimizerUtils::clear_cloudflare_cache([$permalink]); |
| 322 | OptimizerWebPageCache::delete_cache_by_url($permalink); |
| 323 | } |
| 324 | |
| 325 | $redirect_to = (!empty($_SERVER['HTTP_REFERER'])) ? sanitize_text_field($_SERVER['HTTP_REFERER']) : '/wp-admin/edit.php'; |
| 326 | |
| 327 | wp_safe_redirect($redirect_to); |
| 328 | die; |
| 329 | } |
| 330 | |
| 331 | /** |
| 332 | * Validate URL for cache clearing - reject URLs with dangerous characters |
| 333 | * Instead of trying to fix the URL, we reject it to prevent path traversal attacks |
| 334 | * |
| 335 | * @param string $url The URL to validate |
| 336 | * |
| 337 | * @return bool True if URL is safe, false if it contains dangerous characters |
| 338 | */ |
| 339 | private static function is_url_safe_for_cache_clear($url) |
| 340 | { |
| 341 | if (empty($url)) { |
| 342 | return false; |
| 343 | } |
| 344 | |
| 345 | $parsed_url = wp_parse_url($url); |
| 346 | |
| 347 | // Must have host and path |
| 348 | if (!isset($parsed_url['host']) || !isset($parsed_url['path'])) { |
| 349 | return false; |
| 350 | } |
| 351 | |
| 352 | $path = $parsed_url['path']; |
| 353 | |
| 354 | // Reject if contains directory traversal sequences |
| 355 | if (strpos($path, '../') !== false || strpos($path, '..\\') !== false) { |
| 356 | return false; |
| 357 | } |
| 358 | |
| 359 | // Reject if contains null bytes |
| 360 | if (strpos($path, "\0") !== false) { |
| 361 | return false; |
| 362 | } |
| 363 | |
| 364 | // Reject if path contains encoded directory traversal |
| 365 | if (strpos($path, '%2e%2e%2f') !== false || strpos($path, '%2e%2e\\') !== false) { |
| 366 | return false; |
| 367 | } |
| 368 | |
| 369 | // URL is safe |
| 370 | return true; |
| 371 | } |
| 372 | |
| 373 | /** |
| 374 | * Validate that cache path stays within TENWEB_SO_PAGE_CACHE_DIR |
| 375 | * |
| 376 | * @param string $cache_dir The cache directory path to validate |
| 377 | * |
| 378 | * @return string|false Returns normalized path if valid, false otherwise |
| 379 | */ |
| 380 | private static function validate_cache_path($cache_dir) |
| 381 | { |
| 382 | if (empty($cache_dir)) { |
| 383 | return false; |
| 384 | } |
| 385 | |
| 386 | // Normalize paths for comparison |
| 387 | $cache_dir = rtrim($cache_dir, '/') . '/'; |
| 388 | $allowed_dir = rtrim(TENWEB_SO_PAGE_CACHE_DIR, '/') . '/'; |
| 389 | |
| 390 | // First check: ensure the path starts with the allowed directory (string comparison) |
| 391 | // This works even if directories don't exist yet |
| 392 | if (strpos($cache_dir, $allowed_dir) !== 0) { |
| 393 | return false; |
| 394 | } |
| 395 | |
| 396 | // Second check: use realpath if directory exists to resolve symlinks and normalize |
| 397 | $real_cache_dir = realpath($cache_dir); |
| 398 | $real_allowed_dir = realpath($allowed_dir); |
| 399 | |
| 400 | // If both paths exist, verify the resolved path is still within allowed directory |
| 401 | if ($real_cache_dir !== false && $real_allowed_dir !== false) { |
| 402 | if (strpos($real_cache_dir, $real_allowed_dir) !== 0) { |
| 403 | return false; |
| 404 | } |
| 405 | |
| 406 | return $real_cache_dir; |
| 407 | } |
| 408 | |
| 409 | // If directory doesn't exist yet, return the normalized path if it passed string check |
| 410 | // This allows validation of paths that will be created |
| 411 | return $cache_dir; |
| 412 | } |
| 413 | |
| 414 | public function post_row_actions($actions, $post) |
| 415 | { |
| 416 | global $TwoSettings; |
| 417 | |
| 418 | if ($TwoSettings->get_settings('two_page_cache') !== 'on') { |
| 419 | return $actions; |
| 420 | } |
| 421 | |
| 422 | $url = wp_nonce_url(admin_url('admin-post.php?action=two_clear_page_cache&permalink=' . get_permalink($post)), 'two_clear_page_cache'); |
| 423 | $actions['two_clear_page_cache'] = sprintf('<a href="%s">%s</a>', $url, __('Clear page cache', 'tenweb-speed-optimizer')); |
| 424 | |
| 425 | return $actions; |
| 426 | } |
| 427 | |
| 428 | public function transition_post_status($new_status, $old_status, $post) |
| 429 | { |
| 430 | if ($post->post_type !== 'post') { |
| 431 | return; |
| 432 | } |
| 433 | |
| 434 | if ($new_status === $old_status) { |
| 435 | return; |
| 436 | } |
| 437 | |
| 438 | // if status changed from publish (e.g. to trash) or to publish (e.g. from draft to publish) clear page cache |
| 439 | if ($new_status === 'publish' || $old_status === 'publish') { |
| 440 | OptimizerUtils::clear_cloudflare_cache([get_home_url()]); |
| 441 | OptimizerWebPageCache::delete_cache_by_url(get_home_url()); |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | public static function get_instance() |
| 446 | { |
| 447 | if (null == self::$instance) { |
| 448 | self::$instance = new self(); |
| 449 | } |
| 450 | |
| 451 | return self::$instance; |
| 452 | } |
| 453 | } |
| 454 |