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 / vendor / matthiasmullie / minify / src / Minify.php
tenweb-speed-optimizer / vendor / matthiasmullie / minify / src Last commit date
Exceptions 3 years ago CSS.php 2 years ago Exception.php 3 years ago JS.php 2 years ago Minify.php 2 years ago
Minify.php
561 lines
1 <?php
2
3 /**
4 * Abstract minifier class.
5 *
6 * Please report bugs on https://github.com/matthiasmullie/minify/issues
7 *
8 * @author Matthias Mullie <minify@mullie.eu>
9 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
10 * @license MIT License
11 */
12
13 namespace MatthiasMullie\Minify;
14
15 use MatthiasMullie\Minify\Exceptions\IOException;
16 use Psr\Cache\CacheItemInterface;
17
18 /**
19 * Abstract minifier class.
20 *
21 * Please report bugs on https://github.com/matthiasmullie/minify/issues
22 *
23 * @author Matthias Mullie <minify@mullie.eu>
24 * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved
25 * @license MIT License
26 */
27 abstract class Minify
28 {
29 /**
30 * The data to be minified.
31 *
32 * @var string[]
33 */
34 protected $data = array();
35
36 /**
37 * Array of patterns to match.
38 *
39 * @var string[]
40 */
41 protected $patterns = array();
42
43 /**
44 * This array will hold content of strings and regular expressions that have
45 * been extracted from the JS source code, so we can reliably match "code",
46 * without having to worry about potential "code-like" characters inside.
47 *
48 * @internal
49 *
50 * @var string[]
51 */
52 public $extracted = array();
53
54 /**
55 * Init the minify class - optionally, code may be passed along already.
56 */
57 public function __construct(/* $data = null, ... */)
58 {
59 // it's possible to add the source through the constructor as well ;)
60 if (func_num_args()) {
61 call_user_func_array(array($this, 'add'), func_get_args());
62 }
63 }
64
65 /**
66 * Add a file or straight-up code to be minified.
67 *
68 * @param string|string[] $data
69 *
70 * @return static
71 */
72 public function add($data /* $data = null, ... */)
73 {
74 // bogus "usage" of parameter $data: scrutinizer warns this variable is
75 // not used (we're using func_get_args instead to support overloading),
76 // but it still needs to be defined because it makes no sense to have
77 // this function without argument :)
78 $args = array($data) + func_get_args();
79
80 // this method can be overloaded
81 foreach ($args as $data) {
82 if (is_array($data)) {
83 call_user_func_array(array($this, 'add'), $data);
84 continue;
85 }
86
87 // redefine var
88 $data = (string) $data;
89
90 // load data
91 $value = $this->load($data);
92 $key = ($data != $value) ? $data : count($this->data);
93
94 // replace CR linefeeds etc.
95 // @see https://github.com/matthiasmullie/minify/pull/139
96 $value = str_replace(array("\r\n", "\r"), "\n", $value);
97
98 // store data
99 $this->data[$key] = $value;
100 }
101
102 return $this;
103 }
104
105 /**
106 * Add a file to be minified.
107 *
108 * @param string|string[] $data
109 *
110 * @return static
111 *
112 * @throws IOException
113 */
114 public function addFile($data /* $data = null, ... */)
115 {
116 // bogus "usage" of parameter $data: scrutinizer warns this variable is
117 // not used (we're using func_get_args instead to support overloading),
118 // but it still needs to be defined because it makes no sense to have
119 // this function without argument :)
120 $args = array($data) + func_get_args();
121
122 // this method can be overloaded
123 foreach ($args as $path) {
124 if (is_array($path)) {
125 call_user_func_array(array($this, 'addFile'), $path);
126 continue;
127 }
128
129 // redefine var
130 $path = (string) $path;
131
132 // check if we can read the file
133 if (!$this->canImportFile($path)) {
134 throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.');
135 }
136
137 $this->add($path);
138 }
139
140 return $this;
141 }
142
143 /**
144 * Minify the data & (optionally) saves it to a file.
145 *
146 * @param string[optional] $path Path to write the data to
147 *
148 * @return string The minified data
149 */
150 public function minify($path = null)
151 {
152 $content = $this->execute($path);
153
154 // save to path
155 if ($path !== null) {
156 $this->save($content, $path);
157 }
158
159 return $content;
160 }
161
162 /**
163 * Minify & gzip the data & (optionally) saves it to a file.
164 *
165 * @param string[optional] $path Path to write the data to
166 * @param int[optional] $level Compression level, from 0 to 9
167 *
168 * @return string The minified & gzipped data
169 */
170 public function gzip($path = null, $level = 9)
171 {
172 $content = $this->execute($path);
173 $content = gzencode($content, $level, FORCE_GZIP);
174
175 // save to path
176 if ($path !== null) {
177 $this->save($content, $path);
178 }
179
180 return $content;
181 }
182
183 /**
184 * Minify the data & write it to a CacheItemInterface object.
185 *
186 * @param CacheItemInterface $item Cache item to write the data to
187 *
188 * @return CacheItemInterface Cache item with the minifier data
189 */
190 public function cache(CacheItemInterface $item)
191 {
192 $content = $this->execute();
193 $item->set($content);
194
195 return $item;
196 }
197
198 /**
199 * Minify the data.
200 *
201 * @param string[optional] $path Path to write the data to
202 *
203 * @return string The minified data
204 */
205 abstract public function execute($path = null);
206
207 /**
208 * Load data.
209 *
210 * @param string $data Either a path to a file or the content itself
211 *
212 * @return string
213 */
214 protected function load($data)
215 {
216 // check if the data is a file
217 if ($this->canImportFile($data)) {
218 $data = file_get_contents($data);
219
220 // strip BOM, if any
221 if (substr($data, 0, 3) == "\xef\xbb\xbf") {
222 $data = substr($data, 3);
223 }
224 }
225
226 return $data;
227 }
228
229 /**
230 * Save to file.
231 *
232 * @param string $content The minified data
233 * @param string $path The path to save the minified data to
234 *
235 * @throws IOException
236 */
237 protected function save($content, $path)
238 {
239 $handler = $this->openFileForWriting($path);
240
241 $this->writeToFile($handler, $content);
242
243 @fclose($handler);
244 }
245
246 /**
247 * Register a pattern to execute against the source content.
248 *
249 * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work.
250 * If you need that functionality, use a callback instead.
251 *
252 * @param string $pattern PCRE pattern
253 * @param string|callable $replacement Replacement value for matched pattern
254 */
255 protected function registerPattern($pattern, $replacement = '')
256 {
257 // study the pattern, we'll execute it more than once
258 $pattern .= 'S';
259
260 $this->patterns[] = array($pattern, $replacement);
261 }
262
263 /**
264 * Both JS and CSS use the same form of multi-line comment, so putting the common code here.
265 */
266 protected function stripMultilineComments()
267 {
268 // First extract comments we want to keep, so they can be restored later
269 // PHP only supports $this inside anonymous functions since 5.4
270 $minifier = $this;
271 $callback = function ($match) use ($minifier) {
272 $count = count($minifier->extracted);
273 $placeholder = '/*' . $count . '*/';
274 $minifier->extracted[$placeholder] = $match[0];
275
276 return $placeholder;
277 };
278 $this->registerPattern('/
279 # optional newline
280 \n?
281
282 # start comment
283 \/\*
284
285 # comment content
286 (?:
287 # either starts with an !
288 !
289 |
290 # or, after some number of characters which do not end the comment
291 (?:(?!\*\/).)*?
292
293 # there is either a @license or @preserve tag
294 @(?:license|preserve)
295 )
296
297 # then match to the end of the comment
298 .*?\*\/\n?
299
300 /ixs', $callback);
301
302 // Then strip all other comments
303 $this->registerPattern('/\/\*.*?\*\//s', '');
304 }
305
306 /**
307 * We can't "just" run some regular expressions against JavaScript: it's a
308 * complex language. E.g. having an occurrence of // xyz would be a comment,
309 * unless it's used within a string. Of you could have something that looks
310 * like a 'string', but inside a comment.
311 * The only way to accurately replace these pieces is to traverse the JS one
312 * character at a time and try to find whatever starts first.
313 *
314 * @param string $content The content to replace patterns in
315 *
316 * @return string The (manipulated) content
317 */
318 protected function replace($content)
319 {
320 $contentLength = strlen($content);
321 $output = '';
322 $processedOffset = 0;
323 $positions = array_fill(0, count($this->patterns), -1);
324 $matches = array();
325
326 while ($processedOffset < $contentLength) {
327 // find first match for all patterns
328 foreach ($this->patterns as $i => $pattern) {
329 list($pattern, $replacement) = $pattern;
330
331 // we can safely ignore patterns for positions we've unset earlier,
332 // because we know these won't show up anymore
333 if (array_key_exists($i, $positions) == false) {
334 continue;
335 }
336
337 // no need to re-run matches that are still in the part of the
338 // content that hasn't been processed
339 if ($positions[$i] >= $processedOffset) {
340 continue;
341 }
342
343 $match = null;
344 if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) {
345 $matches[$i] = $match;
346
347 // we'll store the match position as well; that way, we
348 // don't have to redo all preg_matches after changing only
349 // the first (we'll still know where those others are)
350 $positions[$i] = $match[0][1];
351 } else {
352 // if the pattern couldn't be matched, there's no point in
353 // executing it again in later runs on this same content;
354 // ignore this one until we reach end of content
355 unset($matches[$i], $positions[$i]);
356 }
357 }
358
359 // no more matches to find: everything's been processed, break out
360 if (!$matches) {
361 // output the remaining content
362 $output .= substr($content, $processedOffset);
363 break;
364 }
365
366 // see which of the patterns actually found the first thing (we'll
367 // only want to execute that one, since we're unsure if what the
368 // other found was not inside what the first found)
369 $matchOffset = min($positions);
370 $firstPattern = array_search($matchOffset, $positions);
371 $match = $matches[$firstPattern];
372
373 // execute the pattern that matches earliest in the content string
374 list(, $replacement) = $this->patterns[$firstPattern];
375
376 // add the part of the input between $processedOffset and the first match;
377 // that content wasn't matched by anything
378 $output .= substr($content, $processedOffset, $matchOffset - $processedOffset);
379 // add the replacement for the match
380 $output .= $this->executeReplacement($replacement, $match);
381 // advance $processedOffset past the match
382 $processedOffset = $matchOffset + strlen($match[0][0]);
383 }
384
385 return $output;
386 }
387
388 /**
389 * If $replacement is a callback, execute it, passing in the match data.
390 * If it's a string, just pass it through.
391 *
392 * @param string|callable $replacement Replacement value
393 * @param array $match Match data, in PREG_OFFSET_CAPTURE form
394 *
395 * @return string
396 */
397 protected function executeReplacement($replacement, $match)
398 {
399 if (!is_callable($replacement)) {
400 return $replacement;
401 }
402 // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects
403 foreach ($match as &$matchItem) {
404 $matchItem = $matchItem[0];
405 }
406
407 return $replacement($match);
408 }
409
410 /**
411 * Strings are a pattern we need to match, in order to ignore potential
412 * code-like content inside them, but we just want all of the string
413 * content to remain untouched.
414 *
415 * This method will replace all string content with simple STRING#
416 * placeholder text, so we've rid all strings from characters that may be
417 * misinterpreted. Original string content will be saved in $this->extracted
418 * and after doing all other minifying, we can restore the original content
419 * via restoreStrings().
420 *
421 * @param string[optional] $chars
422 * @param string[optional] $placeholderPrefix
423 */
424 protected function extractStrings($chars = '\'"', $placeholderPrefix = '')
425 {
426 // PHP only supports $this inside anonymous functions since 5.4
427 $minifier = $this;
428 $callback = function ($match) use ($minifier, $placeholderPrefix) {
429 // check the second index here, because the first always contains a quote
430 if ($match[2] === '') {
431 /*
432 * Empty strings need no placeholder; they can't be confused for
433 * anything else anyway.
434 * But we still needed to match them, for the extraction routine
435 * to skip over this particular string.
436 */
437 return $match[0];
438 }
439
440 $count = count($minifier->extracted);
441 $placeholder = $match[1] . $placeholderPrefix . $count . $match[1];
442 $minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1];
443
444 return $placeholder;
445 };
446
447 /*
448 * The \\ messiness explained:
449 * * Don't count ' or " as end-of-string if it's escaped (has backslash
450 * in front of it)
451 * * Unless... that backslash itself is escaped (another leading slash),
452 * in which case it's no longer escaping the ' or "
453 * * So there can be either no backslash, or an even number
454 * * multiply all of that times 4, to account for the escaping that has
455 * to be done to pass the backslash into the PHP string without it being
456 * considered as escape-char (times 2) and to get it in the regex,
457 * escaped (times 2)
458 */
459 $this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback);
460 }
461
462 /**
463 * This method will restore all extracted data (strings, regexes) that were
464 * replaced with placeholder text in extract*(). The original content was
465 * saved in $this->extracted.
466 *
467 * @param string $content
468 *
469 * @return string
470 */
471 protected function restoreExtractedData($content)
472 {
473 if (!$this->extracted) {
474 // nothing was extracted, nothing to restore
475 return $content;
476 }
477
478 $content = strtr($content, $this->extracted);
479
480 $this->extracted = array();
481
482 return $content;
483 }
484
485 /**
486 * Check if the path is a regular file and can be read.
487 *
488 * @param string $path
489 *
490 * @return bool
491 */
492 protected function canImportFile($path)
493 {
494 $parsed = parse_url($path);
495 if (
496 // file is elsewhere
497 isset($parsed['host'])
498 // file responds to queries (may change, or need to bypass cache)
499 || isset($parsed['query'])
500 ) {
501 return false;
502 }
503
504 try {
505 return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path);
506 }
507 // catch openbasedir exceptions which are not caught by @ on is_file()
508 catch (\Exception $e) {
509 return false;
510 }
511 }
512
513 /**
514 * Attempts to open file specified by $path for writing.
515 *
516 * @param string $path The path to the file
517 *
518 * @return resource Specifier for the target file
519 *
520 * @throws IOException
521 */
522 protected function openFileForWriting($path)
523 {
524 if ($path === '' || ($handler = @fopen($path, 'w')) === false) {
525 throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.');
526 }
527
528 return $handler;
529 }
530
531 /**
532 * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions.
533 *
534 * @param resource $handler The resource to write to
535 * @param string $content The content to write
536 * @param string $path The path to the file (for exception printing only)
537 *
538 * @throws IOException
539 */
540 protected function writeToFile($handler, $content, $path = '')
541 {
542 if (
543 !is_resource($handler)
544 || ($result = @fwrite($handler, $content)) === false
545 || ($result < strlen($content))
546 ) {
547 throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.');
548 }
549 }
550
551 protected static function str_replace_first($search, $replace, $subject)
552 {
553 $pos = strpos($subject, $search);
554 if ($pos !== false) {
555 return substr_replace($subject, $replace, $pos, strlen($search));
556 }
557
558 return $subject;
559 }
560 }
561