PluginProbe
ShopBuilder – WooCommerce Builder For Elementor / 3.2.6
ShopBuilder – WooCommerce Builder For Elementor v3.2.6
3.4.2 3.4.1 3.4.0 2.0.1 2.0.2 2.0.3 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 All 63 releases
shopbuilder / vendor / matthiasmullie / minify / src / Minify.php

Minify.php in ShopBuilder – WooCommerce Builder For Elementor 3.2.6, at vendor/matthiasmullie/minify/src/Minify.php

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