PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.23
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.23
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-js-minifier.php

class-js-minifier.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.23, at code-minification/class-js-minifier.php

196 lines 6.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Metasync_JS_Minifier
4 * Conservative regex-based JS minification engine (no AST parsing).
5 * Hooks into script_loader_tag to rewrite enqueued scripts to cached minified versions.
6 *
7 * @package Search Atlas SEO
8 * @copyright Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
9 * @since 2.5.23
10 */
11
12 if (!defined('ABSPATH')) {
13 exit;
14 }
15
16 class Metasync_JS_Minifier {
17
18 /** @var array */
19 private $settings;
20
21 /** @var array Handles to exclude from minification. */
22 private $excluded_handles = [];
23
24 public function __construct(array $settings) {
25 $this->settings = $settings;
26 $this->excluded_handles = array_filter(
27 array_map('trim', explode(',', $settings['js_exclude_handles'] ?? ''))
28 );
29
30 add_filter('script_loader_tag', [$this, 'maybe_minify_script'], 998, 3);
31 }
32
33 /**
34 * Filter: attempt to minify the script and rewrite the tag to use the cached version.
35 *
36 * @param string $tag The full <script> tag HTML.
37 * @param string $handle The script's registered handle.
38 * @param string $src The script's source URL.
39 * @return string Modified or original tag.
40 */
41 public function maybe_minify_script(string $tag, string $handle, string $src): string {
42 // Skip admin, login, excluded handles, inline scripts
43 if (is_admin() || $this->is_excluded($handle) || $this->is_login_page() || empty($src)) {
44 return $tag;
45 }
46
47 // Only process local files
48 $local_path = $this->url_to_local_path($src);
49 if (!$local_path || !file_exists($local_path)) {
50 return $tag;
51 }
52
53 // Skip already-minified files
54 if (preg_match('/\.min\.js$/i', $local_path)) {
55 return $tag;
56 }
57
58 // Skip ES module files — relocating them breaks relative import paths
59 if ($this->is_es_module($tag, $local_path)) {
60 return $tag;
61 }
62
63 // Check cache
64 $cached = Metasync_Minification_Cache::get_cached_file($handle, 'js', $local_path);
65 if ($cached) {
66 return str_replace($src, $cached['url'], $tag);
67 }
68
69 // Read and minify
70 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
71 $content = file_get_contents($local_path);
72 if ($content === false || strlen($content) === 0) {
73 return $tag;
74 }
75
76 $minified = self::minify($content);
77
78 // Store in cache
79 $cached = Metasync_Minification_Cache::store_cached_file($handle, 'js', $local_path, $minified);
80 if ($cached) {
81 return str_replace($src, $cached['url'], $tag);
82 }
83
84 return $tag;
85 }
86
87 /**
88 * Minify a JavaScript string using conservative regex-based transformations.
89 *
90 * @param string $js Raw JavaScript content.
91 * @return string Minified JavaScript.
92 */
93 public static function minify(string $js): string {
94 // 1. Remove multi-line comments (but preserve conditional compilation comments /*! ... */)
95 $js = preg_replace('/\/\*(?!![\s\S]*?\*\/)[\s\S]*?\*\//', '', $js);
96
97 // 2. Remove single-line comments (careful with URLs and regex literals)
98 // Only remove // comments that are not inside strings or URLs
99 $js = preg_replace('/(?<![:\"\'\\\])\/\/[^\n]*/', '', $js);
100
101 // 3. Remove trailing whitespace per line
102 $js = preg_replace('/[ \t]+$/m', '', $js);
103
104 // 4. Collapse multiple blank lines into one
105 $js = preg_replace('/\n{3,}/', "\n\n", $js);
106
107 // 5. Remove leading whitespace on lines (conservative: only spaces/tabs at start of line)
108 $js = preg_replace('/^\s+/m', '', $js);
109
110 // 6. Collapse multiple spaces into one (within lines)
111 $js = preg_replace('/[ \t]+/', ' ', $js);
112
113 // 7. Remove spaces around operators (conservative set only)
114 $js = preg_replace('/\s*([{};,])\s*/', '$1', $js);
115
116 // 8. Remove empty lines
117 $js = preg_replace('/^\s*$/m', '', $js);
118 $js = preg_replace('/\n+/', "\n", $js);
119
120 return trim($js);
121 }
122
123 /**
124 * Check if a script is an ES module (uses import/export with relative paths).
125 * Relocating ES modules to the cache directory breaks their relative imports.
126 */
127 private function is_es_module(string $tag, string $local_path): bool {
128 // Check if the script tag has type="module"
129 if (preg_match('/type\s*=\s*["\']module["\']/', $tag)) {
130 return true;
131 }
132
133 // Read beginning of file to check for ES module import/export syntax
134 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
135 $head = file_get_contents($local_path, false, null, 0, 2048);
136 if ($head === false) {
137 return false;
138 }
139
140 // Detect relative import paths like: from"./js/..." or from'../something'
141 if (preg_match('/\bimport\b.+?\bfrom\s*["\']\.\.?\//', $head)) {
142 return true;
143 }
144
145 return false;
146 }
147
148 /**
149 * Check if a handle is excluded.
150 */
151 private function is_excluded(string $handle): bool {
152 return in_array($handle, $this->excluded_handles, true);
153 }
154
155 /**
156 * Convert a URL to a local filesystem path.
157 *
158 * @param string $url The script URL.
159 * @return string|false Local path or false if not local.
160 */
161 private function url_to_local_path(string $url) {
162 $url = strtok($url, '?');
163
164 if (strpos($url, '//') === 0) {
165 $url = 'https:' . $url;
166 }
167
168 $site_url = site_url();
169 $content_url = content_url();
170 $abspath = ABSPATH;
171
172 if (strpos($url, $site_url) === 0) {
173 $relative = str_replace($site_url, '', $url);
174 return $abspath . ltrim($relative, '/');
175 }
176
177 if (strpos($url, $content_url) === 0) {
178 $relative = str_replace($content_url, '', $url);
179 return WP_CONTENT_DIR . '/' . ltrim($relative, '/');
180 }
181
182 if (strpos($url, '/wp-content/') === 0 || strpos($url, '/wp-includes/') === 0) {
183 return $abspath . ltrim($url, '/');
184 }
185
186 return false;
187 }
188
189 /**
190 * Check if current request is the login page.
191 */
192 private function is_login_page(): bool {
193 return in_array($GLOBALS['pagenow'] ?? '', ['wp-login.php', 'wp-register.php'], true);
194 }
195 }
196