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 / OptimizerWebPageCacheWP.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
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