OptimizerWebPageCache.php
7 months ago
OptimizerWebPageCacheWP.php
7 months ago
OptimizerWebPageValidations.php
1 year ago
advanced-cache.php
3 years ago
OptimizerWebPageCache.php
494 lines
| 1 | <?php |
| 2 | |
| 3 | namespace TenWebOptimizer\WebPageCache; |
| 4 | |
| 5 | use TenWebOptimizer\OptimizerSettings; |
| 6 | use TenWebOptimizer\OptimizerUtils; |
| 7 | |
| 8 | /* |
| 9 | * Base class other (more-specific) classes inherit from. |
| 10 | */ |
| 11 | if (!defined('ABSPATH')) { |
| 12 | exit; |
| 13 | } |
| 14 | |
| 15 | /** |
| 16 | * The cache structure |
| 17 | * wp-content/tw_optimize/page_cache/{host}/{request_uri}/{query_args} |
| 18 | * e.g. wp-content/tw_optimize/page_cache/{wp.local}/sample-page/#lang=us |
| 19 | * |
| 20 | * */ |
| 21 | class OptimizerWebPageCache |
| 22 | { |
| 23 | protected static $instance = null; |
| 24 | |
| 25 | protected static $config = null; |
| 26 | |
| 27 | protected $accept_gzip = false; |
| 28 | |
| 29 | protected $validator = null; |
| 30 | |
| 31 | protected $cache_dir = null; |
| 32 | |
| 33 | protected $cache_file = null; |
| 34 | |
| 35 | protected $cache_headers_file = null; |
| 36 | |
| 37 | protected $cache_file_gzip = null; |
| 38 | |
| 39 | public function __construct() |
| 40 | { |
| 41 | $two_settings = self::get_config('two_settings'); |
| 42 | $two_settings = json_decode($two_settings, true); |
| 43 | |
| 44 | if (!isset($two_settings['two_connected']) || $two_settings['two_connected'] !== '1') { |
| 45 | return; |
| 46 | } |
| 47 | |
| 48 | if (!defined('TENWEB_SO_PER_USER_CACHE_LIFETIME')) { |
| 49 | define('TENWEB_SO_PER_USER_CACHE_LIFETIME', 172800); // 2 days |
| 50 | } |
| 51 | |
| 52 | if (!defined('TENWEB_SO_PER_USER_CACHE_SIZE_LIMIT')) { |
| 53 | define('TENWEB_SO_PER_USER_CACHE_SIZE_LIMIT', 3000000000); // 3GB |
| 54 | } |
| 55 | |
| 56 | // Additional check for the old users who have cache enabled with old configs. |
| 57 | if (!defined('TWO_PLUGIN_DIR_CACHE')) { |
| 58 | define('TWO_PLUGIN_DIR_CACHE', WP_CONTENT_DIR . '/plugins/tenweb-speed-optimizer/'); |
| 59 | } |
| 60 | require_once __DIR__ . '/OptimizerWebPageValidations.php'; |
| 61 | |
| 62 | require_once TWO_PLUGIN_DIR_CACHE . '/includes/OptimizerUtils.php'; |
| 63 | require_once TWO_PLUGIN_DIR_CACHE . '/includes/OptimizerCache.php'; |
| 64 | require_once TWO_PLUGIN_DIR_CACHE . '/includes/OptimizerUrl.php'; |
| 65 | require_once TWO_PLUGIN_DIR_CACHE . '/includes/OptimizerSettings.php'; |
| 66 | global $TwoSettings; |
| 67 | $TwoSettings = OptimizerSettings::get_instance(); |
| 68 | $this->validator = OptimizerWebPageValidations::get_instance(); |
| 69 | |
| 70 | if ($this->can_process_page()) { |
| 71 | $this->init(); |
| 72 | $this->maybe_clear_all_cache(); |
| 73 | $this->process_page(); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | protected function process_page() |
| 78 | { |
| 79 | $file_to_serve = $this->get_cached_file_to_serve(); |
| 80 | |
| 81 | if ($file_to_serve !== null && !$this->maybe_clear_cache($file_to_serve)) { |
| 82 | $this->serve_cached_file($file_to_serve); |
| 83 | } |
| 84 | |
| 85 | // cache content |
| 86 | ob_start([$this, 'cache_content']); |
| 87 | } |
| 88 | |
| 89 | protected function init() |
| 90 | { |
| 91 | global $TwoSettings; |
| 92 | $accept_encoding_value = ''; |
| 93 | |
| 94 | if (isset($this->request_headers()['ACCEPT-ENCODING'])) { |
| 95 | $accept_encoding = $this->request_headers()['ACCEPT-ENCODING']; |
| 96 | $accept_encoding_value = $accept_encoding && strpos($accept_encoding, 'gzip') !== false; |
| 97 | } |
| 98 | |
| 99 | $serve_gzip = $TwoSettings->get_settings('two_serve_gzip') == 'yes' ? true : false; |
| 100 | $serve_gzip_in_any_case = $TwoSettings->get_settings('two_empty_encoding_serve_gzip') == 'yes' ? true : false; |
| 101 | |
| 102 | if ($serve_gzip && ($accept_encoding_value || $serve_gzip_in_any_case)) { |
| 103 | $this->accept_gzip = true; |
| 104 | } |
| 105 | |
| 106 | $this->cache_dir = $this->get_cache_file_dir(); |
| 107 | // don't use index.html to be able to use delete_all_cache_file() function |
| 108 | $this->cache_file = $this->cache_dir . '/source.html'; |
| 109 | $this->cache_headers_file = $this->cache_dir . '/headers.json'; |
| 110 | $this->cache_file_gzip = $this->cache_dir . '/source.html.gzip'; |
| 111 | } |
| 112 | |
| 113 | protected function request_headers() |
| 114 | { |
| 115 | $arh = []; |
| 116 | $rx_http = '/\AHTTP_/'; |
| 117 | |
| 118 | foreach ($_SERVER as $key => $val) { |
| 119 | if (preg_match($rx_http, $key)) { |
| 120 | $arh_key = preg_replace($rx_http, '', $key); |
| 121 | // do some nasty string manipulations to restore the original letter case |
| 122 | // this should work in most cases |
| 123 | $rx_matches = explode('_', $arh_key); |
| 124 | |
| 125 | if (count($rx_matches) > 0 and strlen($arh_key) > 2) { |
| 126 | foreach ($rx_matches as $ak_key => $ak_val) { |
| 127 | $rx_matches[$ak_key] = ucfirst($ak_val); |
| 128 | } |
| 129 | $arh_key = implode('-', $rx_matches); |
| 130 | } |
| 131 | $arh[$arh_key] = $val; |
| 132 | } |
| 133 | } |
| 134 | |
| 135 | return $arh; |
| 136 | } |
| 137 | |
| 138 | protected function get_cached_file_to_serve() |
| 139 | { |
| 140 | if ($this->accept_gzip && file_exists($this->cache_file_gzip) && is_readable($this->cache_file_gzip)) { |
| 141 | return $this->cache_file_gzip; |
| 142 | } |
| 143 | |
| 144 | if (file_exists($this->cache_file) && is_readable($this->cache_file)) { |
| 145 | return $this->cache_file; |
| 146 | } |
| 147 | |
| 148 | return null; |
| 149 | } |
| 150 | |
| 151 | protected function serve_cached_file($file_to_serve) |
| 152 | { |
| 153 | $file_time = filemtime($file_to_serve); |
| 154 | |
| 155 | if (!isset($_COOKIE['wcml_client_currency'])) { |
| 156 | // if this is wcml_client_currency cookie, browser should not use it's cache |
| 157 | header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $file_time) . ' GMT'); |
| 158 | } |
| 159 | header('X-TWO-PAGE-CACHED: 1'); |
| 160 | |
| 161 | if (file_exists($this->cache_headers_file)) { |
| 162 | $cache_headers = json_decode(file_get_contents($this->cache_headers_file)); // phpcs:ignore |
| 163 | |
| 164 | foreach ($cache_headers as $header) { |
| 165 | header($header); |
| 166 | } |
| 167 | } |
| 168 | |
| 169 | // If the cache has not been modified since $_SERVER["HTTP_IF_MODIFIED_SINCE"], then send 304 not modified and |
| 170 | // browser use cached html (cached in browser) |
| 171 | if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $file_time)) { // phpcs:ignore |
| 172 | header(strip_tags($_SERVER['SERVER_PROTOCOL']) . ' 304 Not Modified', true, 304); // phpcs:ignore |
| 173 | header('Expires: ' . gmdate('D, d M Y H:i:s') . ' GMT'); |
| 174 | header('Cache-Control: no-cache, must-revalidate'); |
| 175 | die; |
| 176 | } |
| 177 | |
| 178 | if (substr($file_to_serve, -5) === '.gzip') { |
| 179 | readgzfile($file_to_serve); |
| 180 | } else { |
| 181 | readfile($file_to_serve); |
| 182 | } |
| 183 | |
| 184 | die; |
| 185 | } |
| 186 | |
| 187 | protected function cache_content($buffer) |
| 188 | { |
| 189 | if (!is_dir($this->cache_dir)) { |
| 190 | if (!mkdir($concurrentDirectory = $this->cache_dir, 0777, true) && !is_dir($concurrentDirectory)) { // phpcs:ignore |
| 191 | return $buffer; |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | if (!is_writable($this->cache_dir) || !is_readable($this->cache_dir)) { // phpcs:ignore |
| 196 | return $buffer; |
| 197 | } |
| 198 | |
| 199 | if (!$this->validator->valid_buffer_to_cache($buffer)) { |
| 200 | return $buffer; |
| 201 | } |
| 202 | |
| 203 | $buffer .= "\n<!-- =^..^= Cached =^..^= -->"; |
| 204 | |
| 205 | file_put_contents($this->cache_file, $buffer); // phpcs:ignore |
| 206 | file_put_contents($this->cache_headers_file, json_encode(headers_list())); // phpcs:ignore |
| 207 | file_put_contents($this->cache_file_gzip, gzencode($buffer)); // phpcs:ignore |
| 208 | |
| 209 | header('X-TWO-PAGE-CACHED: 0'); |
| 210 | |
| 211 | return $buffer; |
| 212 | } |
| 213 | |
| 214 | protected function can_process_page() |
| 215 | { |
| 216 | if (!$this->validator->allowed_request_method()) { |
| 217 | return false; |
| 218 | } |
| 219 | |
| 220 | if (!$this->validator->allowed_wp_page()) { |
| 221 | return false; |
| 222 | } |
| 223 | |
| 224 | if ($this->validator->rejected_file()) { |
| 225 | return false; |
| 226 | } |
| 227 | |
| 228 | if ($this->validator->rejected_cookie()) { |
| 229 | return false; |
| 230 | } |
| 231 | |
| 232 | if ($this->validator->reject_uri()) { |
| 233 | return false; |
| 234 | } |
| 235 | |
| 236 | if ($this->validator->reject_ua()) { |
| 237 | return false; |
| 238 | } |
| 239 | |
| 240 | if (!$this->validator->allowed_query_string()) { |
| 241 | return false; |
| 242 | } |
| 243 | |
| 244 | if ($this->validator->has_donotcachepage()) { |
| 245 | return false; |
| 246 | } |
| 247 | |
| 248 | if (!$this->validator->allowed_by_optimizer()) { |
| 249 | return false; |
| 250 | } |
| 251 | |
| 252 | return true; |
| 253 | } |
| 254 | |
| 255 | protected function maybe_clear_cache($file_to_serve) |
| 256 | { |
| 257 | global $TwoSettings; |
| 258 | $cache_life_time = intval($TwoSettings->get_settings('two_page_cache_life_time')); |
| 259 | |
| 260 | if (!$cache_life_time) { |
| 261 | $cache_life_time = $TwoSettings->get_default_setting('two_page_cache_life_time'); |
| 262 | } |
| 263 | $per_user_cache = $TwoSettings->get_settings('two_page_cache_user'); |
| 264 | |
| 265 | if ($per_user_cache === 'on') { |
| 266 | $cache_life_time = TENWEB_SO_PER_USER_CACHE_LIFETIME; |
| 267 | } |
| 268 | |
| 269 | if ((time() - filemtime($file_to_serve) < $cache_life_time)) { |
| 270 | return false; |
| 271 | } |
| 272 | |
| 273 | $is_home = self::is_home(explode('?', strip_tags($_SERVER['REQUEST_URI']))[0]); // phpcs:ignore |
| 274 | $cache_dir_without_get_args = explode('/#', $this->cache_dir)[0]; |
| 275 | |
| 276 | if (substr($cache_dir_without_get_args, -1) !== '/') { |
| 277 | $cache_dir_without_get_args .= '/'; |
| 278 | } |
| 279 | |
| 280 | self::delete_page_cache($cache_dir_without_get_args, $is_home); |
| 281 | |
| 282 | return true; |
| 283 | } |
| 284 | |
| 285 | public function get_cache_file_dir() |
| 286 | { |
| 287 | $request_uri = explode('?', strip_tags($_SERVER['REQUEST_URI']))[0]; // phpcs:ignore |
| 288 | $cache_file_dir = self::get_cache_dir_for_page(strip_tags($_SERVER['HTTP_HOST']), $request_uri); // phpcs:ignore |
| 289 | |
| 290 | $cookies = $this->validator->get_cookies(); |
| 291 | |
| 292 | if (!empty($cookies)) { |
| 293 | $cookie_string = '@'; |
| 294 | |
| 295 | foreach ($cookies as $cookie_name => $cookie_value) { |
| 296 | $cookie_string .= $cookie_name . '=' . $cookie_value . '&'; |
| 297 | } |
| 298 | |
| 299 | $cookie_string = rtrim($cookie_string, '&'); |
| 300 | $cache_file_dir .= $cookie_string . '/'; |
| 301 | } |
| 302 | |
| 303 | $query_params = $this->validator->get_query_params(); |
| 304 | |
| 305 | if (empty($query_params)) { |
| 306 | return $cache_file_dir; |
| 307 | } |
| 308 | |
| 309 | $query_string = '#'; |
| 310 | |
| 311 | foreach ($query_params as $param_name => $param_value) { |
| 312 | $query_string .= $param_name . '=' . $param_value . '&'; |
| 313 | } |
| 314 | |
| 315 | $query_string = rtrim($query_string, '&'); |
| 316 | |
| 317 | return $cache_file_dir . $query_string . '/'; |
| 318 | } |
| 319 | |
| 320 | public static function delete_cache_by_post_id($post_id) |
| 321 | { |
| 322 | self::delete_cache_by_url(get_permalink($post_id)); |
| 323 | } |
| 324 | |
| 325 | public static function delete_cache_by_url($url) |
| 326 | { |
| 327 | $parsed_url = wp_parse_url($url); |
| 328 | $is_home_url = false; |
| 329 | |
| 330 | if (isset($parsed_url['path'])) { |
| 331 | $is_home_url = self::is_home($parsed_url['path']); |
| 332 | } |
| 333 | self::delete_page_cache(self::get_cache_dir_for_page_from_url($url), $is_home_url); |
| 334 | } |
| 335 | |
| 336 | public static function delete_page_cache($dir, $is_home_url) |
| 337 | { |
| 338 | // Validate directory path before deletion |
| 339 | if (empty($dir)) { |
| 340 | return; |
| 341 | } |
| 342 | |
| 343 | // Ensure the directory is within the allowed cache directory |
| 344 | $real_dir = realpath($dir); |
| 345 | $real_allowed_dir = realpath(TENWEB_SO_PAGE_CACHE_DIR); |
| 346 | |
| 347 | if ($real_dir === false || $real_allowed_dir === false) { |
| 348 | return; |
| 349 | } |
| 350 | |
| 351 | // Prevent directory traversal - ensure path is within cache directory |
| 352 | if (strpos($real_dir, $real_allowed_dir) !== 0) { |
| 353 | return; |
| 354 | } |
| 355 | |
| 356 | if (is_dir($dir)) { |
| 357 | foreach (scandir($dir) as $file) { |
| 358 | if ($file === '.' || $file === '..') { |
| 359 | continue; |
| 360 | } |
| 361 | |
| 362 | $path = $dir . $file; |
| 363 | |
| 364 | // Additional safety check for each path |
| 365 | $real_path = realpath($path); |
| 366 | |
| 367 | if ($real_path === false || strpos($real_path, $real_allowed_dir) !== 0) { |
| 368 | continue; |
| 369 | } |
| 370 | |
| 371 | if (is_file($path)) { |
| 372 | unlink($path); // phpcs:ignore |
| 373 | } else { |
| 374 | $not_allow_delete = []; |
| 375 | |
| 376 | if ($is_home_url) { |
| 377 | if (substr($file, 0, 1) === '#') { |
| 378 | \TenWebOptimizer\OptimizerUtils::delete_all_cache_file($path, $not_allow_delete); |
| 379 | } |
| 380 | } else { |
| 381 | \TenWebOptimizer\OptimizerUtils::delete_all_cache_file($path, $not_allow_delete); |
| 382 | } |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | // if dir is empty |
| 387 | if (is_dir($dir)) { |
| 388 | $dir_arr = scandir($dir); |
| 389 | |
| 390 | if (is_array($dir_arr) && count($dir_arr) === 2) { |
| 391 | rmdir($dir); // phpcs:ignore |
| 392 | } |
| 393 | } |
| 394 | } |
| 395 | } |
| 396 | |
| 397 | public static function delete_all_cached_pages() |
| 398 | { |
| 399 | \TenWebOptimizer\OptimizerUtils::delete_all_cache_file(TENWEB_SO_PAGE_CACHE_DIR); |
| 400 | |
| 401 | return !is_dir(TENWEB_SO_PAGE_CACHE_DIR); |
| 402 | } |
| 403 | |
| 404 | public static function get_cache_dir_for_page($host, $request_uri) |
| 405 | { |
| 406 | $is_home_page = self::is_home($request_uri); |
| 407 | $cache_dir_name = ''; |
| 408 | |
| 409 | if (!$is_home_page) { |
| 410 | $cache_dir_name = rtrim($request_uri, '/'); |
| 411 | $cache_dir_name = ltrim($cache_dir_name, '/'); |
| 412 | } |
| 413 | global $TwoSettings; |
| 414 | $cache_hash = ''; |
| 415 | |
| 416 | if ('on' === $TwoSettings->get_settings('two_page_cache_user')) { |
| 417 | // phpcs:ignore WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE |
| 418 | $cookies = $_COOKIE; |
| 419 | $cookie_name = array_filter(array_keys($cookies), function ($cookie) { |
| 420 | return strpos($cookie, 'wordpress_logged_in_') === 0; |
| 421 | }); |
| 422 | |
| 423 | if (! empty($cookie_name)) { |
| 424 | $cookie_name = reset($cookie_name); |
| 425 | $cookie_value = $cookies[ $cookie_name ]; |
| 426 | $username = explode('|', $cookie_value)[ 0 ]; |
| 427 | } |
| 428 | |
| 429 | if (! empty($username) && isset($cookies[ 'tenweb_so_page_cache_hash' ])) { |
| 430 | $unique_hash = $cookies[ 'tenweb_so_page_cache_hash' ]; |
| 431 | |
| 432 | // This is an additional check to make sure the cookies are identical, |
| 433 | // it will pass if both cookies are just identical, |
| 434 | // needs to be improved to make sure user is logged in. |
| 435 | if (md5($cookie_value) === $unique_hash) { |
| 436 | $cache_hash = '-' . $username . '-' . $unique_hash; |
| 437 | } |
| 438 | } |
| 439 | } |
| 440 | |
| 441 | // add $host, to support multisite to |
| 442 | $cache_dir = TENWEB_SO_PAGE_CACHE_DIR . $host . $cache_hash . '/'; |
| 443 | |
| 444 | if ($cache_dir_name) { |
| 445 | $cache_dir .= $cache_dir_name . '/'; |
| 446 | } |
| 447 | |
| 448 | return $cache_dir; |
| 449 | } |
| 450 | |
| 451 | public static function get_cache_dir_for_page_from_url($url) |
| 452 | { |
| 453 | $parsed_url = wp_parse_url($url); |
| 454 | |
| 455 | if (!isset($parsed_url['host']) || !isset($parsed_url['path'])) { |
| 456 | return TENWEB_SO_PAGE_CACHE_DIR; |
| 457 | } |
| 458 | |
| 459 | return self::get_cache_dir_for_page($parsed_url['host'], $parsed_url['path']); |
| 460 | } |
| 461 | |
| 462 | public static function get_config($config_name) |
| 463 | { |
| 464 | if (self::$config === null) { |
| 465 | self::$config = json_decode(file_get_contents(WP_CONTENT_DIR . '/10web-page-cache-config/config.json')); |
| 466 | } |
| 467 | |
| 468 | return self::$config->$config_name; |
| 469 | } |
| 470 | |
| 471 | public static function is_home($request_uri) |
| 472 | { |
| 473 | return empty(ltrim($request_uri, '/')); |
| 474 | } |
| 475 | |
| 476 | public static function get_instance() |
| 477 | { |
| 478 | if (null == self::$instance) { |
| 479 | self::$instance = new self(); |
| 480 | } |
| 481 | |
| 482 | return self::$instance; |
| 483 | } |
| 484 | |
| 485 | private function maybe_clear_all_cache() |
| 486 | { |
| 487 | global $TwoSettings; |
| 488 | |
| 489 | if ($TwoSettings->get_settings('two_page_cache_user') === 'on' && OptimizerUtils::dirsize(TENWEB_SO_PAGE_CACHE_DIR) > TENWEB_SO_PER_USER_CACHE_SIZE_LIMIT) { |
| 490 | self::delete_all_cached_pages(); |
| 491 | } |
| 492 | } |
| 493 | } |
| 494 |