PluginProbe ʕ •ᴥ•ʔ
10Web Booster – Website speed optimization, Cache & Page Speed optimizer / trunk
10Web Booster – Website speed optimization, Cache & Page Speed optimizer vtrunk
2.33.0 2.30.5 2.30.7 2.30.9 2.31.10 2.31.8 2.32.11 2.32.21 2.32.3 2.32.4 2.32.7 2.6.31 2.6.40 2.6.42 2.6.7 2.7.37 2.7.44 2.7.47 2.8.18 2.8.19 2.8.32 2.8.34 2.8.35 2.9.23 2.9.24 2.9.25 2.9.27 v2.27.4 trunk 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.17 2.0.18 2.0.21 2.0.22 2.0.25 2.0.26 2.0.27 2.0.3 2.0.7 2.0.9 2.10.46 2.10.65 2.10.66 2.10.68 2.11.41 2.11.42 2.11.43 2.12.15 2.12.21 2.12.22 2.12.23 2.12.26 2.13.37 2.13.40 2.13.41 2.13.42 2.13.44 2.13.45 2.13.47 2.14.49 2.14.50 2.15.18 2.17.21 2.17.23 2.18.17 2.19.44 2.19.45 2.19.46 2.19.49 2.2.12 2.2.15 2.2.16 2.2.18 2.2.8 2.20.31 2.20.32 2.20.33 2.21.11 2.21.12 2.21.16 2.21.25 2.22.32 2.23.13 2.23.15 2.23.16 2.23.18 2.24.12 2.24.14 2.24.18 2.25.14 2.26.6 2.28.10 2.28.13 2.28.14 2.28.7 2.29.1 2.29.2 2.29.3 2.3.0 2.3.1 2.3.2 2.3.3 2.30.18
tenweb-speed-optimizer / includes / WebPageCache / OptimizerWebPageCache.php
tenweb-speed-optimizer / includes / WebPageCache Last commit date
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