PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / trunk
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score vtrunk
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
powered-cache / includes / file-optimizer.php

file-optimizer.php in Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score trunk, at includes/file-optimizer.php

446 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ported from https://github.com/Automattic/nginx-http-concat
4 *
5 * Concatenation script inspired by Nginx's ngx_http_concat and Apache's modconcat modules.
6 *
7 * It follows the same pattern for enabling the concatenation. It uses two ?, like this:
8 * http://example.com/??style1.css,style2.css,foo/style3.css
9 *
10 * If a third ? is present it's treated as version string. Like this:
11 * http://example.com/??style1.css,style2.css,foo/style3.css?v=102234
12 *
13 * It will also replace the relative paths in CSS files with absolute paths.
14 *
15 * @package PoweredCache
16 */
17
18 namespace PoweredCache\FileOptimizer;
19
20 // phpcs:disable
21
22 use PoweredCache\Dependencies\MatthiasMullie\Minify\CSS;
23 use PoweredCache\Dependencies\MatthiasMullie\Minify\JS;
24
25 /**
26 * PSR-4-ish autoloading
27 *
28 * @since 2.0
29 */
30 spl_autoload_register(
31 function ( $class ) {
32 // project-specific namespace prefix.
33 $prefix = 'PoweredCache\\';
34
35 // base directory for the namespace prefix.
36 $base_dir = __DIR__ . '/classes/';
37
38 // does the class use the namespace prefix?
39 $len = strlen( $prefix );
40
41 if ( strncmp( $prefix, $class, $len ) !== 0 ) {
42 return;
43 }
44
45 $relative_class = substr( $class, $len );
46
47 $file = $base_dir . str_replace( '\\', '/', $relative_class ) . '.php';
48
49 // if the file exists, require it.
50 if ( file_exists( $file ) ) {
51 require $file;
52 }
53 }
54 );
55
56 /* Config */
57 $concat_max_files = 150;
58 $concat_unique = true;
59 $concat_types = array(
60 'css' => 'text/css',
61 'js' => 'application/javascript',
62 );
63
64 $wp_content_base = '/wp-content';
65 if ( isset( $_SERVER['SCRIPT_NAME'] ) && false === strpos( $_SERVER['SCRIPT_NAME'], '/wp-content' ) ) {
66 $script_name = explode( '/', ltrim( $_SERVER['SCRIPT_NAME'], '/' ) );
67 if ( ! empty( $script_name[0] ) ) {
68 $wp_content_base = '/' . $script_name[0];
69 }
70 }
71
72 $current_dir = file_optimizer_normalize_path( realpath( dirname( __DIR__ ) ) );
73
74 /* Constants */
75 // By default determine the document root from this scripts path in the plugins dir (you can hardcode this define)
76 define( 'CONCAT_FILES_ROOT', substr( $current_dir, 0, strpos( $current_dir, $wp_content_base ) ) );
77 define( 'POWERED_CACHE_FO_CACHE_DIR', CONCAT_FILES_ROOT . $wp_content_base.'/cache/min/' );
78 define( 'POWERED_CACHE_FO_DEBUG', false );
79
80 if ( ! defined( 'POWERED_CACHE_FO_DISABLE_CACHE_HEADERS' ) ) {
81 define( 'POWERED_CACHE_FO_DISABLE_CACHE_HEADERS', false );
82 }
83
84 if ( ! file_exists( POWERED_CACHE_FO_CACHE_DIR ) ) {
85 mkdir( POWERED_CACHE_FO_CACHE_DIR, 0775, true );
86 }
87
88 function concat_http_status_exit( $status ) {
89 switch ( $status ) {
90 case 200:
91 $text = 'OK';
92 break;
93 case 400:
94 $text = 'Bad Request';
95 break;
96 case 403:
97 $text = 'Forbidden';
98 break;
99 case 404:
100 $text = 'Not found';
101 break;
102 case 500:
103 $text = 'Internal Server Error';
104 break;
105 default:
106 $text = '';
107 }
108
109 $protocol = $_SERVER['SERVER_PROTOCOL'];
110 if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) {
111 $protocol = 'HTTP/1.0';
112 }
113
114 @header( "$protocol $status $text", true, $status );
115 exit();
116 }
117
118 function concat_get_mtype( $file ) {
119 global $concat_types;
120
121 $lastdot_pos = strrpos( $file, '.' );
122 if ( false === $lastdot_pos ) {
123 return false;
124 }
125
126 $ext = substr( $file, $lastdot_pos + 1 );
127
128 return isset( $concat_types[ $ext ] ) ? $concat_types[ $ext ] : false;
129 }
130
131 function concat_get_path( $uri ) {
132 if ( ! strlen( $uri ) ) {
133 maybe_add_debug_log( sprintf( "File Optimizer 400 - could not retrieve file path for uri %s", $uri ) );
134 concat_http_status_exit( 400 );
135 }
136
137 if ( false !== strpos( $uri, '..' ) || false !== strpos( $uri, "\0" ) ) {
138 maybe_add_debug_log( sprintf( "File Optimizer 400 - could not retrieve file path for uri %s", $uri ) );
139 concat_http_status_exit( 400 );
140 }
141
142 return CONCAT_FILES_ROOT . ( '/' != $uri[0] ? '/' : '' ) . $uri;
143 }
144
145 function relative_path_replace( $buf, $dirpath ) {
146 // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
147 $buf = preg_replace(
148 '/(:?\s*url\s*\()\s*(?:\'|")?\s*([^\/\'"\s\)](?:(?<!data:|http:|https:|[\(\'"]#|%23).)*)[\'"\s]*\)/isU',
149 '$1' . ( '/' === $dirpath ? '/' : $dirpath . '/' ) . '$2)',
150 $buf
151 );
152
153 return $buf;
154 }
155
156 /* Main() */
157 if ( ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ) ) ) {
158 maybe_add_debug_log( sprintf( "File Optimizer 400 - unsupported request method: %s", $_SERVER['REQUEST_METHOD'] ) );
159 concat_http_status_exit( 400 );
160 }
161
162 // /_static/??/foo/bar.css,/foo1/bar/baz.css?m=293847g
163 // or
164 // /_static/??-eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
165 $args = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY );
166 if ( ! $args || false === strpos( $args, '?' ) ) {
167 maybe_add_debug_log( sprintf( "File Optimizer 400 - empty query arg or not ? exists" ) );
168 concat_http_status_exit( 400 );
169 }
170
171 $args = substr( $args, strpos( $args, '?' ) + 1 );
172 $args = str_replace( [ '&minify=1', '&minify=0' ], '', $args ); // remove minify parameter
173
174 // /foo/bar.css,/foo1/bar/baz.css?m=293847g
175 // or
176 // -eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
177 if ( '-' == $args[0] ) {
178 $args = @gzuncompress( base64_decode( substr( $args, 1 ) ) );
179
180 // Invalid data, abort!
181 if ( false === $args ) {
182 maybe_add_debug_log( sprintf( "File Optimizer 400 - Invalid Data" ) );
183 concat_http_status_exit( 400 );
184 }
185 }
186
187 // /foo/bar.css,/foo1/bar/baz.css?m=293847g
188 $version_string_pos = strpos( $args, '?' );
189 if ( false !== $version_string_pos ) {
190 $args = substr( $args, 0, $version_string_pos );
191 }
192
193 // /foo/bar.css,/foo1/bar/baz.css
194 $args = explode( ',', $args );
195 if ( ! $args ) {
196 maybe_add_debug_log( sprintf( "File Optimizer 400 - Empty args" ) );
197 concat_http_status_exit( 400 );
198 }
199
200 // array('/wp-content/foo/bar.css','//cdn.cname.com/wp-content/foo/bar.css')
201 // get real path when it masked from cdn
202 foreach ( $args as $index => $arg ) {
203 if ( 0 === stripos( $arg, '//' ) ) {
204 $args[ $index ] = parse_url( str_replace( '//', 'http://', $arg ), PHP_URL_PATH );
205 }
206 }
207
208 // array( '/foo/bar.css', '/foo1/bar/baz.css' )
209 if ( 0 == count( $args ) || count( $args ) > $concat_max_files ) {
210 maybe_add_debug_log( sprintf( "File Optimizer 400 - Concatenating too many or zero file: %s files on queue", count( $args ) ) );
211 concat_http_status_exit( 400 );
212 }
213
214 // If we're in a subdirectory context, use that as the root.
215 // We can't assume that the root serves the same content as the subdir.
216 $subdir_path_prefix = '';
217 $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
218 $_static_index = strpos( $request_path, '/_static/' );
219 if ( $_static_index > 0 ) {
220 $subdir_path_prefix = substr( $request_path, 0, $_static_index );
221 }
222 unset( $request_path, $_static_index );
223
224 $last_modified = 0;
225 $pre_output = '';
226 $output = '';
227
228 $do_minify = (bool) stripos( $_SERVER['REQUEST_URI'], 'minify=1' );
229 $hash = sha1( $_SERVER['REQUEST_URI'] );
230 $latest_file = end( $args );
231
232 $cache_file_name = POWERED_CACHE_FO_CACHE_DIR . $hash;
233 if ( 'application/javascript' == concat_get_mtype( $latest_file ) ) {
234 $cache_file_name .= '.js';
235 } elseif ( 'text/css' == concat_get_mtype( $latest_file ) ) {
236 $cache_file_name .= '.css';
237 }
238
239 if ( file_exists( $cache_file_name ) ) {
240 $buf = @file_get_contents( $cache_file_name );
241 if ( $buf ) {
242 $stat = stat( $cache_file_name );
243
244 set_cache_headers();
245 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $stat['mtime'] ) . ' GMT' );
246 header( 'Content-Length: ' . ( strlen( $pre_output ) + strlen( $buf ) ) );
247 header( 'Content-Type: ' . concat_get_mtype( $latest_file ) );
248
249 echo $pre_output . $buf;
250 exit;
251 }
252 }
253
254 foreach ( $args as $uri ) {
255
256 $fullpath = concat_get_path( $uri );
257
258 if ( ! file_exists( $fullpath ) ) {
259 maybe_add_debug_log( sprintf( "File Optimizer 404 - Missing file: %s", $fullpath ) );
260 concat_http_status_exit( 404 );
261 }
262
263 $mime_type = concat_get_mtype( $fullpath );
264 if ( ! in_array( $mime_type, $concat_types ) ) {
265 maybe_add_debug_log( sprintf( "File Optimizer 400 - Unsupported mime type: %s", $mime_type ) );
266 concat_http_status_exit( 400 );
267 }
268
269 if ( $concat_unique ) {
270 if ( ! isset( $last_mime_type ) ) {
271 $last_mime_type = $mime_type;
272 }
273
274 if ( $last_mime_type != $mime_type ) {
275 maybe_add_debug_log( sprintf( "File Optimizer 400 - Different mime type: last mime %s vs current mime: %s", $last_mime_type, $mime_type ) );
276 concat_http_status_exit( 400 );
277 }
278 }
279
280 $stat = stat( $fullpath );
281 if ( false === $stat ) {
282 maybe_add_debug_log( sprintf( "File Optimizer 500 - false stat" ) );
283 concat_http_status_exit( 500 );
284 }
285
286 if ( $stat['mtime'] > $last_modified ) {
287 $last_modified = $stat['mtime'];
288 }
289
290 $buf = file_get_contents( $fullpath );
291 if ( false === $buf ) {
292 maybe_add_debug_log( sprintf( "File Optimizer 500 - false buffer" ) );
293 concat_http_status_exit( 500 );
294 }
295
296 if ( 'text/css' == $mime_type ) {
297 $dirpath = $subdir_path_prefix . dirname( $uri );
298
299 // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
300 $buf = relative_path_replace( $buf, $dirpath );
301
302 // AlphaImageLoader(...src='relative/path/to/file'...) -> AlphaImageLoader(...src='/absolute/path/to/file'...)
303 $buf = preg_replace(
304 '/(Microsoft.AlphaImageLoader\s*\([^\)]*src=(?:\'|")?)([^\/\'"\s\)](?:(?<!http:|https:).)*)\)/isU',
305 '$1' . ( $dirpath == '/' ? '/' : $dirpath . '/' ) . '$2)',
306 $buf
307 );
308
309 // The @charset rules must be on top of the output
310 if ( 0 === strpos( $buf, '@charset' ) ) {
311 preg_replace_callback(
312 '/(?P<charset_rule>@charset\s+[\'"][^\'"]+[\'"];)/i',
313 function ( $match ) {
314 global $pre_output;
315
316 if ( 0 === strpos( $pre_output, '@charset' ) ) {
317 return '';
318 }
319
320 $pre_output = $match[0] . "\n" . $pre_output;
321
322 return '';
323 },
324 $buf
325 );
326 }
327
328 // Move the @import rules on top of the concatenated output.
329 // Only @charset rule are allowed before them.
330 if ( false !== strpos( $buf, '@import' ) ) {
331 $buf = preg_replace_callback(
332 '/(?P<pre_path>@import\s+(?:url\s*\()?[\'"\s]*)(?P<path>[^\'"\s](?:https?:\/\/.+\/?)?.+?)(?P<post_path>[\'"\s\)]*;)/i',
333 function ( $match ) use ( $dirpath ) {
334 global $pre_output;
335
336 if ( 0 !== strpos( $match['path'], 'http' ) && '/' != $match['path'][0] ) {
337 $pre_output .= $match['pre_path'] . ( $dirpath == '/' ? '/' : $dirpath . '/' ) .
338 $match['path'] . $match['post_path'] . "\n";
339 } else {
340 $pre_output .= $match[0] . "\n";
341 }
342
343 return '';
344 },
345 $buf
346 );
347 }
348 }
349
350 if ( 'application/javascript' == $mime_type ) {
351 $output .= "$buf;\n";
352 } else {
353 $output .= "$buf";
354 }
355 }
356
357 if ( $do_minify && false === stripos( $uri, '.min' ) ) {
358 if ( 'application/javascript' == $mime_type ) {
359 $js_minify = new JS( $output );
360 $output = $js_minify->minify();
361 } elseif ( 'text/css' == $mime_type ) {
362 $css_minify = new CSS( $output );
363 $output = $css_minify->minify();
364 }
365 }
366
367 set_cache_headers();
368 header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT' );
369 header( 'Content-Length: ' . ( strlen( $pre_output ) + strlen( $output ) ) );
370 header( "Content-Type: $mime_type" );
371
372 echo $pre_output . $output;
373
374 $cache_file_name = POWERED_CACHE_FO_CACHE_DIR . $hash;
375 if ( 'application/javascript' == $mime_type ) {
376 $cache_file_name .= '.js';
377 } elseif ( 'text/css' == $mime_type ) {
378 $cache_file_name .= '.css';
379 }
380
381 file_put_contents( $cache_file_name, $pre_output . $output );
382
383
384 /**
385 * Normalize a filesystem path.
386 *
387 * On windows systems, replaces backslashes with forward slashes
388 * and forces upper-case drive letters.
389 * Allows for two leading slashes for Windows network shares, but
390 * ensures that all other duplicate slashes are reduced to a single.
391 *
392 * @param string $path Path to normalize.
393 *
394 * @return string Normalized path.
395 * @see wp_normalize_path
396 *
397 */
398 function file_optimizer_normalize_path( $path ) {
399 $wrapper = '';
400
401 // Standardise all paths to use '/'.
402 $path = str_replace( '\\', '/', $path );
403
404 // Replace multiple slashes down to a singular, allowing for network shares having two slashes.
405 $path = preg_replace( '|(?<=.)/+|', '/', $path );
406
407 // Windows paths should uppercase the drive letter.
408 if ( ':' === substr( $path, 1, 1 ) ) {
409 $path = ucfirst( $path );
410 }
411
412 return $wrapper . $path;
413 }
414
415 /**
416 * Maybe add debug message
417 *
418 * @param string $message Debug message
419 *
420 * @since 2.3
421 */
422 function maybe_add_debug_log( $message ) {
423 if ( defined( 'POWERED_CACHE_FO_DEBUG' ) && POWERED_CACHE_FO_DEBUG ) {
424 error_log( $message );
425 }
426 }
427
428
429 /**
430 * Add cache headers
431 *
432 * @return void
433 * @since 2.5.3
434 */
435 function set_cache_headers() {
436 if ( defined( 'POWERED_CACHE_FO_DISABLE_CACHE_HEADERS' ) && POWERED_CACHE_FO_DISABLE_CACHE_HEADERS ) {
437 return;
438 }
439
440 $ttl_in_seconds = 31536000; // 365 days
441 $ttl = gmdate( "D, d M Y H:i:s", time() + $ttl_in_seconds ) . " GMT";
442
443 header( "Expires: $ttl" );
444 header( "Cache-Control: max-age=$ttl_in_seconds" );
445 }
446