PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.20
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.20
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / code-minification / class-minification-cache.php

class-minification-cache.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.20, at code-minification/class-minification-cache.php

287 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Metasync_Minification_Cache
4 * Manages file-based cache for minified CSS/JS assets.
5 *
6 * @package Search Atlas SEO
7 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
8 * @since 2.5.23
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 class Metasync_Minification_Cache {
16
17 const CACHE_DIR_NAME = 'metasync-speed';
18 const CRON_HOOK = 'metasync_speed_cache_cleanup';
19
20 /**
21 * Get the base cache directory path.
22 */
23 public static function get_cache_dir(): string {
24 return WP_CONTENT_DIR . '/cache/' . self::CACHE_DIR_NAME . '/';
25 }
26
27 /**
28 * Get the base cache directory URL.
29 */
30 public static function get_cache_url(): string {
31 return content_url('/cache/' . self::CACHE_DIR_NAME . '/');
32 }
33
34 /**
35 * Compute cache key for a file.
36 *
37 * @param string $file_path Absolute path to the original file.
38 * @return string 8-char hash.
39 */
40 public static function compute_hash(string $file_path): string {
41 $version = defined('METASYNC_VERSION') ? METASYNC_VERSION : '1.0.0';
42 $mtime = file_exists($file_path) ? (string) filemtime($file_path) : '0';
43 return substr(md5($file_path . $mtime . $version), 0, 8);
44 }
45
46 /**
47 * Get the cached file path for a given handle and type.
48 *
49 * @param string $handle The enqueue handle.
50 * @param string $type 'css' or 'js'.
51 * @param string $hash The content hash.
52 * @return string Absolute path to cached file.
53 */
54 public static function get_cached_path(string $handle, string $type, string $hash): string {
55 $safe_handle = sanitize_file_name($handle);
56 return self::get_cache_dir() . $type . '/' . $safe_handle . '-' . $hash . '.min.' . $type;
57 }
58
59 /**
60 * Get the cached file URL for a given handle and type.
61 *
62 * @param string $handle The enqueue handle.
63 * @param string $type 'css' or 'js'.
64 * @param string $hash The content hash.
65 * @return string URL to the cached file.
66 */
67 public static function get_cached_url(string $handle, string $type, string $hash): string {
68 $safe_handle = sanitize_file_name($handle);
69 return self::get_cache_url() . $type . '/' . $safe_handle . '-' . $hash . '.min.' . $type;
70 }
71
72 /**
73 * Check if a cached file exists and return its path, or false.
74 *
75 * @param string $handle The enqueue handle.
76 * @param string $type 'css' or 'js'.
77 * @param string $file_path Original file path (for hash computation).
78 * @return array|false ['path' => ..., 'url' => ..., 'hash' => ...] or false.
79 */
80 public static function get_cached_file(string $handle, string $type, string $file_path) {
81 $hash = self::compute_hash($file_path);
82 $cached_path = self::get_cached_path($handle, $type, $hash);
83
84 if (file_exists($cached_path)) {
85 return [
86 'path' => $cached_path,
87 'url' => self::get_cached_url($handle, $type, $hash),
88 'hash' => $hash,
89 ];
90 }
91
92 return false;
93 }
94
95 /**
96 * Store a minified file in cache.
97 *
98 * @param string $handle The enqueue handle.
99 * @param string $type 'css' or 'js'.
100 * @param string $file_path Original file path (for hash computation).
101 * @param string $content Minified content to store.
102 * @return array|false ['path' => ..., 'url' => ..., 'hash' => ...] or false on failure.
103 */
104 public static function store_cached_file(string $handle, string $type, string $file_path, string $content) {
105 $hash = self::compute_hash($file_path);
106 $cached_path = self::get_cached_path($handle, $type, $hash);
107 $dir = dirname($cached_path);
108
109 if (!wp_mkdir_p($dir)) {
110 return false;
111 }
112
113 // Write .htaccess to protect the cache directory (deny PHP execution, allow static assets).
114 $base_dir = self::get_cache_dir();
115 $htaccess = $base_dir . '.htaccess';
116 if (!file_exists($htaccess)) {
117 $rules = "Options -Indexes\n";
118 $rules .= "<FilesMatch \"\.php$\">\n";
119 $rules .= " deny from all\n";
120 $rules .= "</FilesMatch>\n";
121 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
122 file_put_contents($htaccess, $rules);
123 }
124
125 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
126 if (file_put_contents($cached_path, $content) === false) {
127 return false;
128 }
129
130 return [
131 'path' => $cached_path,
132 'url' => self::get_cached_url($handle, $type, $hash),
133 'hash' => $hash,
134 ];
135 }
136
137 /**
138 * Purge all cached minification files.
139 */
140 public static function purge_all(): bool {
141 $cache_dir = self::get_cache_dir();
142
143 if (!is_dir($cache_dir)) {
144 return true;
145 }
146
147 return self::delete_directory_contents($cache_dir);
148 }
149
150 /**
151 * Purge expired cached files (older than TTL days).
152 *
153 * @param int $ttl_days Number of days after which files are considered expired.
154 */
155 public static function purge_expired(int $ttl_days = 30): int {
156 $cache_dir = self::get_cache_dir();
157 $deleted = 0;
158
159 if (!is_dir($cache_dir)) {
160 return $deleted;
161 }
162
163 $expiry_time = time() - ($ttl_days * DAY_IN_SECONDS);
164
165 foreach (['css', 'js'] as $type) {
166 $type_dir = $cache_dir . $type . '/';
167 if (!is_dir($type_dir)) {
168 continue;
169 }
170
171 $files = glob($type_dir . '*.min.' . $type);
172 if (!$files) {
173 continue;
174 }
175
176 foreach ($files as $file) {
177 if (filemtime($file) < $expiry_time) {
178 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
179 if (unlink($file)) {
180 $deleted++;
181 }
182 }
183 }
184 }
185
186 return $deleted;
187 }
188
189 /**
190 * Get cache statistics.
191 *
192 * @return array ['total_files' => int, 'total_size' => int, 'css_files' => int, 'js_files' => int]
193 */
194 public static function get_cache_stats(): array {
195 $stats = [
196 'total_files' => 0,
197 'total_size' => 0,
198 'css_files' => 0,
199 'js_files' => 0,
200 'css_size' => 0,
201 'js_size' => 0,
202 ];
203
204 $cache_dir = self::get_cache_dir();
205
206 if (!is_dir($cache_dir)) {
207 return $stats;
208 }
209
210 foreach (['css', 'js'] as $type) {
211 $type_dir = $cache_dir . $type . '/';
212 if (!is_dir($type_dir)) {
213 continue;
214 }
215
216 $files = glob($type_dir . '*.min.' . $type);
217 if (!$files) {
218 continue;
219 }
220
221 foreach ($files as $file) {
222 $size = filesize($file);
223 $stats['total_files']++;
224 $stats['total_size'] += $size;
225 $stats[$type . '_files']++;
226 $stats[$type . '_size'] += $size;
227 }
228 }
229
230 return $stats;
231 }
232
233 /**
234 * Schedule the daily cache cleanup cron job.
235 */
236 public static function schedule_cleanup(): void {
237 if (!wp_next_scheduled(self::CRON_HOOK)) {
238 wp_schedule_event(time(), 'daily', self::CRON_HOOK);
239 }
240 }
241
242 /**
243 * Unschedule the daily cache cleanup cron job.
244 */
245 public static function unschedule_cleanup(): void {
246 $timestamp = wp_next_scheduled(self::CRON_HOOK);
247 if ($timestamp) {
248 wp_unschedule_event($timestamp, self::CRON_HOOK);
249 }
250 }
251
252 /**
253 * Cron callback: purge expired files.
254 */
255 public static function cron_cleanup(): void {
256 $settings = Metasync_Minification_Settings::get_settings();
257 $ttl = (int) ($settings['cache_ttl_days'] ?? 30);
258 self::purge_expired($ttl);
259 }
260
261 /**
262 * Recursively delete contents of a directory (but keep the directory itself).
263 */
264 private static function delete_directory_contents(string $dir): bool {
265 if (!is_dir($dir)) {
266 return false;
267 }
268
269 $items = new \RecursiveIteratorIterator(
270 new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
271 \RecursiveIteratorIterator::CHILD_FIRST
272 );
273
274 foreach ($items as $item) {
275 if ($item->isDir()) {
276 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
277 rmdir($item->getRealPath());
278 } else {
279 // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
280 unlink($item->getRealPath());
281 }
282 }
283
284 return true;
285 }
286 }
287