PluginProbe
Page Optimize / trunk
Page Optimize vtrunk
trunk 0.4.2 0.4.3 0.4.4 0.4.6 0.4.7 0.5.0 0.5.1 0.5.2 0.5.3 0.5.4 0.5.5 0.5.6 0.5.7 0.5.8 0.6.0 0.6.1 0.6.2 0.6.3
page-optimize / service.php

service.php in Page Optimize trunk, at service.php

725 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 $page_optimize_types = array(
4 'css' => 'text/css',
5 'js' => 'application/javascript'
6 );
7
8 function page_optimize_service_request() {
9 $use_cache = defined( 'PAGE_OPTIMIZE_CACHE_DIR' ) && ! empty( PAGE_OPTIMIZE_CACHE_DIR );
10 if ( $use_cache && ! is_dir( PAGE_OPTIMIZE_CACHE_DIR ) && ! mkdir( PAGE_OPTIMIZE_CACHE_DIR, 0775, true ) ) {
11 $use_cache = false;
12 error_log( sprintf(
13 /* translators: a filesystem path to a directory */
14 __( "Disabling page-optimize cache. Unable to create cache directory '%s'.", page_optimize_get_text_domain() ),
15 PAGE_OPTIMIZE_CACHE_DIR
16 ) );
17 }
18 if ( $use_cache && ( ! is_dir( PAGE_OPTIMIZE_CACHE_DIR ) || ! is_writable( PAGE_OPTIMIZE_CACHE_DIR ) || ! is_executable( PAGE_OPTIMIZE_CACHE_DIR ) ) ) {
19 $use_cache = false;
20 error_log( sprintf(
21 /* translators: a filesystem path to a directory */
22 __( "Disabling page-optimize cache. Unable to write to cache directory '%s'.", page_optimize_get_text_domain() ),
23 PAGE_OPTIMIZE_CACHE_DIR
24 ) );
25 }
26
27 if ( $use_cache ) {
28 $request_uri_hash = md5( $_SERVER['REQUEST_URI'] );
29 $cache_file = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-$request_uri_hash";
30 $cache_file_meta = PAGE_OPTIMIZE_CACHE_DIR . "/page-optimize-cache-meta-$request_uri_hash";
31
32 if ( file_exists( $cache_file ) ) {
33 if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
34 if ( strtotime( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) < filemtime( $cache_file ) ) {
35 header( 'HTTP/1.1 304 Not Modified' );
36 exit;
37 }
38 }
39
40 if ( file_exists( $cache_file_meta ) ) {
41 $meta = json_decode( file_get_contents( $cache_file_meta ) );
42 if ( null !== $meta && isset( $meta->headers ) ) {
43 foreach ( $meta->headers as $header ) {
44 header( $header );
45 }
46 }
47 }
48
49 $etag = '"' . md5( file_get_contents( $cache_file ) ) . '"';
50
51 header( 'X-Page-Optimize: cached' );
52 header( 'Cache-Control: max-age=' . 31536000 );
53 header( 'ETag: ' . $etag );
54
55 echo file_get_contents( $cache_file ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
56 die();
57 }
58 }
59
60 $output = page_optimize_build_output();
61 $content = $output['content'];
62 $headers = $output['headers'];
63
64 foreach( $headers as $header ) {
65 header( $header );
66 }
67 header( 'X-Page-Optimize: uncached' );
68 header( 'Cache-Control: max-age=' . 31536000 );
69 header( 'ETag: "' . md5( $content ) . '"' );
70
71 echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- We need to trust this unfortunately.
72
73 if ( $use_cache ) {
74 file_put_contents( $cache_file, $content );
75 file_put_contents( $cache_file_meta, json_encode( array( 'headers' => $headers ) ) );
76 }
77
78 die();
79 }
80
81 function page_optimize_build_output() {
82 global $page_optimize_types;
83
84 require_once __DIR__ . '/cssmin/cssmin.php';
85
86 /* Config */
87 $concat_max_files = 300;
88 $concat_unique = true;
89
90 /* Main() */
91 if ( ! in_array( $_SERVER['REQUEST_METHOD'], array( 'GET', 'HEAD' ) ) ) {
92 page_optimize_status_exit( 400 );
93 }
94
95 // /_static/??/foo/bar.css,/foo1/bar/baz.css?m=293847g
96 // or
97 // /_static/??-eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
98 $args = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_QUERY );
99 if ( ! $args || false === strpos( $args, '?' ) ) {
100 page_optimize_status_exit( 400 );
101 }
102
103 $args = substr( $args, strpos( $args, '?' ) + 1 );
104
105 // /foo/bar.css,/foo1/bar/baz.css?m=293847g
106 // or
107 // -eJzTT8vP109KLNJLLi7W0QdyDEE8IK4CiVjn2hpZGluYmKcDABRMDPM=
108 if ( '-' == $args[0] ) {
109 $args = @gzuncompress( base64_decode( substr( $args, 1 ) ) );
110
111 // Invalid data, abort!
112 if ( false === $args ) {
113 page_optimize_status_exit( 400 );
114 }
115 }
116
117 // /foo/bar.css,/foo1/bar/baz.css?m=293847g
118 $version_string_pos = strpos( $args, '?' );
119 if ( false !== $version_string_pos ) {
120 $args = substr( $args, 0, $version_string_pos );
121 }
122
123 // /foo/bar.css,/foo1/bar/baz.css
124 $args = explode( ',', $args );
125 if ( ! $args ) {
126 page_optimize_status_exit( 400 );
127 }
128
129 // array( '/foo/bar.css', '/foo1/bar/baz.css' )
130 if ( 0 == count( $args ) || count( $args ) > $concat_max_files ) {
131 page_optimize_status_exit( 400 );
132 }
133
134 // If we're in a subdirectory context, use that as the root.
135 // We can't assume that the root serves the same content as the subdir.
136 $subdir_path_prefix = '';
137 $request_path = parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH );
138 $_static_index = strpos( $request_path, '/_static/' );
139 if ( $_static_index > 0 ) {
140 $subdir_path_prefix = substr( $request_path, 0, $_static_index );
141 }
142 unset( $request_path, $_static_index );
143
144 $last_modified = 0;
145 $pre_output = '';
146 $output = '';
147
148 $should_minify_css = defined( 'PAGE_OPTIMIZE_CSS_MINIFY' ) && ! empty( PAGE_OPTIMIZE_CSS_MINIFY );
149
150 if ( $should_minify_css ) {
151 $css_minify = new tubalmartin\CssMin\Minifier;
152 }
153
154 foreach ( $args as $uri ) {
155 $fullpath = page_optimize_get_path( $uri );
156
157 if ( ! file_exists( $fullpath ) ) {
158 page_optimize_status_exit( 404 );
159 }
160
161 $mime_type = page_optimize_get_mime_type( $fullpath );
162 if ( ! in_array( $mime_type, $page_optimize_types ) ) {
163 page_optimize_status_exit( 400 );
164 }
165
166 if ( $concat_unique ) {
167 if ( ! isset( $last_mime_type ) ) {
168 $last_mime_type = $mime_type;
169 }
170
171 if ( $last_mime_type != $mime_type ) {
172 page_optimize_status_exit( 400 );
173 }
174 }
175
176 $stat = stat( $fullpath );
177 if ( false === $stat ) {
178 page_optimize_status_exit( 500 );
179 }
180
181 if ( $stat['mtime'] > $last_modified ) {
182 $last_modified = $stat['mtime'];
183 }
184
185 $buf = file_get_contents( $fullpath );
186 if ( false === $buf ) {
187 page_optimize_status_exit( 500 );
188 }
189
190 if ( 'text/css' == $mime_type ) {
191 $dirpath = '/' . ltrim( $subdir_path_prefix . dirname( $uri ), '/' );
192
193 // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
194 $buf = page_optimize_relative_path_replace( $buf, $dirpath );
195
196 // AlphaImageLoader(...src='relative/path/to/file'...) -> AlphaImageLoader(...src='/absolute/path/to/file'...)
197 $buf = preg_replace(
198 '/(Microsoft.AlphaImageLoader\s*\([^\)]*src=(?:\'|")?)([^\/\'"\s\)](?:(?<!http:|https:).)*)\)/isU',
199 '$1' . ( $dirpath == '/' ? '/' : $dirpath . '/' ) . '$2)',
200 $buf
201 );
202
203 // The @charset rules must be on top of the output
204 if ( 0 === stripos( $buf, '@charset' ) ) {
205 $buf = preg_replace_callback(
206 '/(?P<charset_rule>@charset\s+[\'"][^\'"]+[\'"];)/i',
207 function ( $match ) use ( &$pre_output ) {
208
209 if ( 0 === stripos( (string) $pre_output, '@charset' ) ) {
210 return '';
211 }
212
213 $pre_output = $match[0] . "\n" . $pre_output;
214 return '';
215 },
216 $buf
217 );
218 }
219
220 // Move the @import rules on top of the concatenated output.
221 // Only @charset rule are allowed before them.
222 if ( false !== stripos( $buf, '@import' ) ) {
223 // Regex-based hoisting breaks on URLs containing semicolons (like Google Fonts)
224 $buf = page_optimize_hoist_css_import_rules( $buf, $dirpath, $pre_output );
225 }
226
227 if ( $should_minify_css ) {
228 $buf = $css_minify->run( $buf );
229 }
230 }
231
232 if ( $page_optimize_types['js'] === $mime_type ) {
233 $output .= "$buf;\n";
234 } else {
235 $output .= "$buf";
236 }
237 }
238
239 $headers = array(
240 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $last_modified ) . ' GMT',
241 'Content-Length: ' . ( strlen( $pre_output ) + strlen( $output ) ),
242 "Content-Type: $mime_type",
243 );
244
245 return array(
246 'headers' => $headers,
247 'content' => $pre_output . $output,
248 );
249 }
250
251 function page_optimize_status_exit( $status ) {
252 http_response_code( $status );
253 exit;
254 }
255
256 function page_optimize_get_mime_type( $file ) {
257 global $page_optimize_types;
258
259 $lastdot_pos = strrpos( $file, '.' );
260 if ( false === $lastdot_pos ) {
261 return false;
262 }
263
264 $ext = substr( $file, $lastdot_pos + 1 );
265
266 return isset( $page_optimize_types[ $ext ] ) ? $page_optimize_types[ $ext ] : false;
267 }
268
269 function page_optimize_relative_path_replace( $buf, $dirpath ) {
270 // url(relative/path/to/file) -> url(/absolute/and/not/relative/path/to/file)
271 $buf = preg_replace(
272 '/(:?\s*url\s*\()\s*(?:\'|")?\s*([^\/\'"\s\)](?:(?<!data:|http:|https:|[\(\'"]#|%23).)*)[\'"\s]*\)/isU',
273 '$1' . ( $dirpath == '/' ? '/' : $dirpath . '/' ) . '$2)',
274 $buf
275 );
276
277 return $buf;
278 }
279
280 /**
281 * Hoist CSS @import rules from a stylesheet body.
282 *
283 * @param string $buf Stylesheet contents.
284 * @param string $dirpath Directory path used for resolving relative imports.
285 * @param string $pre_output Buffer for hoisted rules.
286 *
287 * @return string Stylesheet body without hoisted @import rules.
288 */
289 function page_optimize_hoist_css_import_rules( $buf, $dirpath, &$pre_output ) {
290 $offset = 0;
291 $new_buf = '';
292 $new_pre_output = '';
293
294 while ( true ) {
295 $import_pos = page_optimize_css_find_next_import_position( $buf, $offset );
296 if ( false === $import_pos ) {
297 break;
298 }
299
300 $rule_end_pos = page_optimize_css_find_import_rule_end( $buf, $import_pos );
301 if ( false === $rule_end_pos ) {
302 // Parsing failed; keep original buffer unchanged.
303 return $buf;
304 }
305
306 $new_buf .= substr( $buf, $offset, $import_pos - $offset );
307 $rule = substr( $buf, $import_pos, $rule_end_pos - $import_pos + 1 );
308
309 $new_pre_output .= page_optimize_css_rewrite_import_rule_path( $rule, $dirpath ) . "\n";
310 $offset = $rule_end_pos + 1;
311 }
312
313 $new_buf .= substr( $buf, $offset );
314 $pre_output .= $new_pre_output;
315
316 return $new_buf;
317 }
318
319 /**
320 * Find the next @import position outside quoted strings and block comments.
321 *
322 * @param string $css Source CSS.
323 * @param int $offset Search offset.
324 *
325 * @return int|false
326 */
327 function page_optimize_css_find_next_import_position( $css, $offset = 0 ) {
328 $length = strlen( $css );
329 $quote = '';
330 $brace_depth = 0;
331
332 for ( $i = $offset; $i < $length; $i++ ) {
333 $char = $css[ $i ];
334
335 if ( '' !== $quote ) {
336 if ( '\\' === $char && $i + 1 < $length ) {
337 $i++;
338 continue;
339 }
340
341 if ( $quote === $char ) {
342 $quote = '';
343 }
344
345 continue;
346 }
347
348 if ( '/' === $char && $i + 1 < $length && '*' === $css[ $i + 1 ] ) {
349 $comment_end = strpos( $css, '*/', $i + 2 );
350 if ( false === $comment_end ) {
351 return false;
352 }
353
354 $i = $comment_end + 1;
355 continue;
356 }
357
358 if ( '\'' === $char || '"' === $char ) {
359 $quote = $char;
360 continue;
361 }
362
363 if ( '{' === $char ) {
364 $brace_depth++;
365 continue;
366 }
367
368 if ( '}' === $char ) {
369 if ( $brace_depth > 0 ) {
370 $brace_depth--;
371 }
372 continue;
373 }
374
375 if ( $brace_depth > 0 ) {
376 continue;
377 }
378
379 if ( '@' !== $char ) {
380 continue;
381 }
382
383 if ( 0 !== strncasecmp( substr( $css, $i, 7 ), '@import', 7 ) ) {
384 continue;
385 }
386
387 $next_char_pos = $i + 7;
388 if ( $next_char_pos < $length && page_optimize_css_is_identifier_char( $css[ $next_char_pos ] ) ) {
389 continue;
390 }
391
392 if ( ! page_optimize_css_is_valid_import_next_char( $css, $next_char_pos, $length ) ) {
393 continue;
394 }
395
396 return $i;
397 }
398
399 return false;
400 }
401
402 /**
403 * Find the semicolon that terminates an @import rule.
404 *
405 * @param string $css CSS source.
406 * @param int $import_pos Position of the @import token.
407 *
408 * @return int|false
409 */
410 function page_optimize_css_find_import_rule_end( $css, $import_pos ) {
411 $length = strlen( $css );
412 $quote = '';
413 $paren_depth = 0;
414
415 for ( $i = $import_pos; $i < $length; $i++ ) {
416 $char = $css[ $i ];
417
418 if ( '' !== $quote ) {
419 if ( '\\' === $char && $i + 1 < $length ) {
420 $i++;
421 continue;
422 }
423
424 if ( $quote === $char ) {
425 $quote = '';
426 }
427
428 continue;
429 }
430
431 if ( '/' === $char && $i + 1 < $length && '*' === $css[ $i + 1 ] ) {
432 $comment_end = strpos( $css, '*/', $i + 2 );
433 if ( false === $comment_end ) {
434 return false;
435 }
436
437 $i = $comment_end + 1;
438 continue;
439 }
440
441 if ( '\'' === $char || '"' === $char ) {
442 $quote = $char;
443 continue;
444 }
445
446 if ( '(' === $char ) {
447 $paren_depth++;
448 continue;
449 }
450
451 if ( ')' === $char ) {
452 if ( $paren_depth > 0 ) {
453 $paren_depth--;
454 }
455 continue;
456 }
457
458 if ( ';' === $char && 0 === $paren_depth ) {
459 return $i;
460 }
461
462 if ( '{' === $char && 0 === $paren_depth ) {
463 return false;
464 }
465 }
466
467 return false;
468 }
469
470 /**
471 * Rewrite relative @import paths using the current file directory.
472 *
473 * @param string $rule Complete @import rule including terminating semicolon.
474 * @param string $dirpath Directory path used for resolving relative imports.
475 *
476 * @return string
477 */
478 function page_optimize_css_rewrite_import_rule_path( $rule, $dirpath ) {
479 $length = strlen( $rule );
480 $import_pos = stripos( $rule, '@import' );
481 if ( false === $import_pos ) {
482 return $rule;
483 }
484
485 $cursor = $import_pos + 7; // strlen('@import').
486 $cursor = page_optimize_css_skip_whitespace_and_comments( $rule, $cursor, $length );
487 if ( false === $cursor ) {
488 return $rule;
489 }
490
491 if ( $cursor >= $length ) {
492 return $rule;
493 }
494
495 $path_pos = null;
496 $path_len = null;
497
498 // Parse either @import "..." or @import url(...)
499 if ( '\'' === $rule[ $cursor ] || '"' === $rule[ $cursor ] ) {
500 $quote = $rule[ $cursor ];
501 $path_pos = $cursor + 1;
502 $cursor++;
503
504 while ( $cursor < $length ) {
505 if ( '\\' === $rule[ $cursor ] && $cursor + 1 < $length ) {
506 $cursor += 2;
507 continue;
508 }
509
510 if ( $quote === $rule[ $cursor ] ) {
511 $path_len = $cursor - $path_pos;
512 break;
513 }
514
515 $cursor++;
516 }
517 } elseif ( 0 === strncasecmp( substr( $rule, $cursor, 3 ), 'url', 3 ) ) {
518 $cursor += 3;
519
520 $cursor = page_optimize_css_skip_whitespace_and_comments( $rule, $cursor, $length );
521 if ( false === $cursor ) {
522 return $rule;
523 }
524
525 if ( $cursor >= $length || '(' !== $rule[ $cursor ] ) {
526 return $rule;
527 }
528
529 $cursor++;
530 $cursor = page_optimize_css_skip_whitespace_and_comments( $rule, $cursor, $length );
531 if ( false === $cursor ) {
532 return $rule;
533 }
534
535 if ( $cursor >= $length ) {
536 return $rule;
537 }
538
539 if ( '\'' === $rule[ $cursor ] || '"' === $rule[ $cursor ] ) {
540 $quote = $rule[ $cursor ];
541 $path_pos = $cursor + 1;
542 $cursor++;
543
544 while ( $cursor < $length ) {
545 if ( '\\' === $rule[ $cursor ] && $cursor + 1 < $length ) {
546 $cursor += 2;
547 continue;
548 }
549
550 if ( $quote === $rule[ $cursor ] ) {
551 $path_len = $cursor - $path_pos;
552 $cursor++;
553 break;
554 }
555
556 $cursor++;
557 }
558
559 if ( null === $path_len ) {
560 return $rule;
561 }
562
563 $cursor = page_optimize_css_skip_whitespace_and_comments( $rule, $cursor, $length );
564 if ( false === $cursor ) {
565 return $rule;
566 }
567
568 if ( $cursor >= $length || ')' !== $rule[ $cursor ] ) {
569 return $rule;
570 }
571 } else {
572 $path_pos = $cursor;
573 while ( $cursor < $length && ')' !== $rule[ $cursor ] ) {
574 $cursor++;
575 }
576
577 if ( $cursor >= $length ) {
578 return $rule;
579 }
580
581 $path_end = $cursor;
582 while ( $path_end > $path_pos && ctype_space( $rule[ $path_end - 1 ] ) ) {
583 $path_end--;
584 }
585
586 $path_len = $path_end - $path_pos;
587 }
588 }
589
590 if ( null === $path_pos || null === $path_len || $path_len <= 0 ) {
591 return $rule;
592 }
593
594 $path = substr( $rule, $path_pos, $path_len );
595 if ( '' === $path || page_optimize_css_import_path_is_absolute( $path ) ) {
596 return $rule;
597 }
598
599 $new_path = ( '/' === $dirpath ? '/' : $dirpath . '/' ) . $path;
600
601 return substr( $rule, 0, $path_pos ) . $new_path . substr( $rule, $path_pos + $path_len );
602 }
603
604 /**
605 * Determine whether the given @import path is absolute.
606 *
607 * @param string $path Import path.
608 *
609 * @return bool
610 */
611 function page_optimize_css_import_path_is_absolute( $path ) {
612 if ( '' === $path ) {
613 return true;
614 }
615
616 if ( '/' === $path[0] ) {
617 return true;
618 }
619
620 return 1 === preg_match( '#^[a-z][a-z0-9+.-]*:#i', $path );
621 }
622
623 /**
624 * Determine whether a character can appear in a CSS identifier.
625 *
626 * @param string $char Single character.
627 *
628 * @return bool
629 */
630 function page_optimize_css_is_identifier_char( $char ) {
631 return ctype_alnum( $char ) || '_' === $char || '-' === $char;
632 }
633
634 /**
635 * Determine whether the next token after "@import" can start a valid rule.
636 *
637 * @param string $css Source CSS.
638 * @param int $next_char_pos Position immediately after "@import".
639 * @param int $length CSS length.
640 *
641 * @return bool
642 */
643 function page_optimize_css_is_valid_import_next_char( $css, $next_char_pos, $length ) {
644 if ( $next_char_pos >= $length ) {
645 return false;
646 }
647
648 $next_char = $css[ $next_char_pos ];
649 if ( ctype_space( $next_char ) || '\'' === $next_char || '"' === $next_char ) {
650 return true;
651 }
652
653 return '/' === $next_char && $next_char_pos + 1 < $length && '*' === $css[ $next_char_pos + 1 ];
654 }
655
656 /**
657 * Skip CSS whitespace and block comments.
658 *
659 * @param string $css CSS source.
660 * @param int $cursor Start offset.
661 * @param int $length CSS length.
662 *
663 * @return int|false Next offset or false on malformed comment.
664 */
665 function page_optimize_css_skip_whitespace_and_comments( $css, $cursor, $length ) {
666 while ( $cursor < $length ) {
667 $char = $css[ $cursor ];
668
669 if ( ctype_space( $char ) ) {
670 $cursor++;
671 continue;
672 }
673
674 if ( '/' === $char && $cursor + 1 < $length && '*' === $css[ $cursor + 1 ] ) {
675 $comment_end = strpos( $css, '*/', $cursor + 2 );
676 if ( false === $comment_end ) {
677 return false;
678 }
679
680 $cursor = $comment_end + 2;
681 continue;
682 }
683
684 break;
685 }
686
687 return $cursor;
688 }
689
690 function page_optimize_get_path( $uri ) {
691 static $dependency_path_mapping;
692
693 if ( ! strlen( $uri ) ) {
694 page_optimize_status_exit( 400 );
695 }
696
697 if ( false !== strpos( $uri, '..' ) || false !== strpos( $uri, "\0" ) ) {
698 page_optimize_status_exit( 400 );
699 }
700
701 if ( defined( 'PAGE_OPTIMIZE_CONCAT_BASE_DIR' ) ) {
702 $path = realpath( PAGE_OPTIMIZE_CONCAT_BASE_DIR . "/$uri" );
703
704 if ( false === $path ) {
705 $path = realpath( PAGE_OPTIMIZE_ABSPATH . "/$uri" );
706 }
707 } else {
708 if ( empty( $dependency_path_mapping ) ) {
709 require_once __DIR__ . '/dependency-path-mapping.php';
710 $dependency_path_mapping = new Page_Optimize_Dependency_Path_Mapping();
711 }
712 $path = $dependency_path_mapping->uri_path_to_fs_path( $uri );
713 }
714
715 if ( false === $path ) {
716 page_optimize_status_exit( 404 );
717 }
718
719 return $path;
720 }
721
722 if ( ! defined( 'PAGE_OPTIMIZE_SKIP_SERVICE_REQUEST' ) ) {
723 page_optimize_service_request();
724 }
725