PluginProbe
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance / 3.2.5
WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance v3.2.5
4.7.0 4.6.1 4.6.0 4.5.5 4.5.4 4.5.3 4.5.2 3.2.20 3.2.21 3.2.22 3.2.3 3.2.5 3.2.6 3.2.7 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.7.0 3.7.1 All 111 releases
wp-optimize / vendor / matthiasmullie / minify / src / Minify.php

Minify.php in WP-Optimize – Cache, Compress images, Minify & Clean database to boost page speed & performance 3.2.5, at vendor/matthiasmullie/minify/src/Minify.php

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