| @@ -1,7 +1,8 @@ | ||
| 1 | 1 | <?php |
| 2 | + | |
| 2 | 3 | /** |
| 3 | - * Abstract minifier class | |
| 4 | + * Abstract minifier class. | |
| 4 | 5 | * |
| 5 | 6 | * Please report bugs on https://github.com/matthiasmullie/minify/issues |
| 6 | 7 | * |
| 7 | 8 | * @author Matthias Mullie <[email protected]> |
| @@ -7,11 +8,13 @@ | ||
| 7 | 8 | * @author Matthias Mullie <[email protected]> |
| 8 | 9 | * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved |
| 9 | 10 | * @license MIT License |
| 10 | 11 | */ |
| 12 | + | |
| 11 | 13 | namespace MatthiasMullie\Minify; |
| 12 | 14 | |
| 13 | 15 | use MatthiasMullie\Minify\Exceptions\IOException; |
| 16 | +use MatthiasMullie\Minify\Exceptions\PatternMatchException; | |
| 14 | 17 | use Psr\Cache\CacheItemInterface; |
| 15 | 18 | |
| 16 | 19 | /** |
| 17 | 20 | * Abstract minifier class. |
| @@ -17,9 +20,8 @@ | ||
| 17 | 20 | * Abstract minifier class. |
| 18 | 21 | * |
| 19 | 22 | * Please report bugs on https://github.com/matthiasmullie/minify/issues |
| 20 | 23 | * |
| 21 | - * @package Minify | |
| 22 | 24 | * @author Matthias Mullie <[email protected]> |
| 23 | 25 | * @copyright Copyright (c) 2012, Matthias Mullie. All rights reserved |
| 24 | 26 | * @license MIT License |
| 25 | 27 | */ |
| @@ -43,8 +45,10 @@ | ||
| 43 | 45 | * This array will hold content of strings and regular expressions that have |
| 44 | 46 | * been extracted from the JS source code, so we can reliably match "code", |
| 45 | 47 | * without having to worry about potential "code-like" characters inside. |
| 46 | 48 | * |
| 49 | + * @internal | |
| 50 | + * | |
| 47 | 51 | * @var string[] |
| 48 | 52 | */ |
| 49 | 53 | public $extracted = array(); |
| 50 | 54 | |
| @@ -127,9 +131,9 @@ | ||
| 127 | 131 | $path = (string) $path; |
| 128 | 132 | |
| 129 | 133 | // check if we can read the file |
| 130 | 134 | if (!$this->canImportFile($path)) { |
| 131 | - throw new IOException('The file "'.$path.'" could not be opened for reading. Check if PHP has enough permissions.'); | |
| 135 | + throw new IOException('The file "' . $path . '" could not be opened for reading. Check if PHP has enough permissions.'); | |
| 132 | 136 | } |
| 133 | 137 | |
| 134 | 138 | $this->add($path); |
| 135 | 139 | } |
| @@ -226,9 +230,9 @@ | ||
| 226 | 230 | /** |
| 227 | 231 | * Save to file. |
| 228 | 232 | * |
| 229 | 233 | * @param string $content The minified data |
| 230 | - * @param string $path The path to save the minified data to | |
| 234 | + * @param string $path The path to save the minified data to | |
| 231 | 235 | * |
| 232 | 236 | * @throws IOException |
| 233 | 237 | */ |
| 234 | 238 | protected function save($content, $path) |
| @@ -245,9 +249,9 @@ | ||
| 245 | 249 | * |
| 246 | 250 | * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. |
| 247 | 251 | * If you need that functionality, use a callback instead. |
| 248 | 252 | * |
| 249 | - * @param string $pattern PCRE pattern | |
| 253 | + * @param string $pattern PCRE pattern | |
| 250 | 254 | * @param string|callable $replacement Replacement value for matched pattern |
| 251 | 255 | */ |
| 252 | 256 | protected function registerPattern($pattern, $replacement = '') |
| 253 | 257 | { |
| @@ -257,8 +261,47 @@ | ||
| 257 | 261 | $this->patterns[] = array($pattern, $replacement); |
| 258 | 262 | } |
| 259 | 263 | |
| 260 | 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 | + /** | |
| 261 | 304 | * We can't "just" run some regular expressions against JavaScript: it's a |
| 262 | 305 | * complex language. E.g. having an occurrence of // xyz would be a comment, |
| 263 | 306 | * unless it's used within a string. Of you could have something that looks |
| 264 | 307 | * like a 'string', but inside a comment. |
| @@ -267,8 +310,10 @@ | ||
| 267 | 310 | * |
| 268 | 311 | * @param string $content The content to replace patterns in |
| 269 | 312 | * |
| 270 | 313 | * @return string The (manipulated) content |
| 314 | + * | |
| 315 | + * @throws PatternMatchException | |
| 271 | 316 | */ |
| 272 | 317 | protected function replace($content) |
| 273 | 318 | { |
| 274 | 319 | $contentLength = strlen($content); |
| @@ -294,9 +339,10 @@ | ||
| 294 | 339 | continue; |
| 295 | 340 | } |
| 296 | 341 | |
| 297 | 342 | $match = null; |
| 298 | - if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { | |
| 343 | + $matchResult = preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset); | |
| 344 | + if ($matchResult) { | |
| 299 | 345 | $matches[$i] = $match; |
| 300 | 346 | |
| 301 | 347 | // we'll store the match position as well; that way, we |
| 302 | 348 | // don't have to redo all preg_matches after changing only |
| @@ -302,8 +348,13 @@ | ||
| 302 | 348 | // don't have to redo all preg_matches after changing only |
| 303 | 349 | // the first (we'll still know where those others are) |
| 304 | 350 | $positions[$i] = $match[0][1]; |
| 305 | 351 | } else { |
| 352 | + if ($matchResult === false) { | |
| 353 | + throw PatternMatchException::fromLastError( | |
| 354 | + "Failed to match pattern '$pattern' at $processedOffset" | |
| 355 | + ); | |
| 356 | + } | |
| 306 | 357 | // if the pattern couldn't be matched, there's no point in |
| 307 | 358 | // executing it again in later runs on this same content; |
| 308 | 359 | // ignore this one until we reach end of content |
| 309 | 360 | unset($matches[$i], $positions[$i]); |
| @@ -343,9 +394,9 @@ | ||
| 343 | 394 | * If $replacement is a callback, execute it, passing in the match data. |
| 344 | 395 | * If it's a string, just pass it through. |
| 345 | 396 | * |
| 346 | 397 | * @param string|callable $replacement Replacement value |
| 347 | - * @param array $match Match data, in PREG_OFFSET_CAPTURE form | |
| 398 | + * @param array $match Match data, in PREG_OFFSET_CAPTURE form | |
| 348 | 399 | * |
| 349 | 400 | * @return string |
| 350 | 401 | */ |
| 351 | 402 | protected function executeReplacement($replacement, $match) |
| @@ -356,8 +407,9 @@ | ||
| 356 | 407 | // convert $match from the PREG_OFFSET_CAPTURE form to the form the callback expects |
| 357 | 408 | foreach ($match as &$matchItem) { |
| 358 | 409 | $matchItem = $matchItem[0]; |
| 359 | 410 | } |
| 411 | + | |
| 360 | 412 | return $replacement($match); |
| 361 | 413 | } |
| 362 | 414 | |
| 363 | 415 | /** |
| @@ -390,15 +442,20 @@ | ||
| 390 | 442 | return $match[0]; |
| 391 | 443 | } |
| 392 | 444 | |
| 393 | 445 | $count = count($minifier->extracted); |
| 394 | - $placeholder = $match[1].$placeholderPrefix.$count.$match[1]; | |
| 395 | - $minifier->extracted[$placeholder] = $match[1].$match[2].$match[1]; | |
| 446 | + $placeholder = $match[1] . $placeholderPrefix . $count . $match[1]; | |
| 447 | + $minifier->extracted[$placeholder] = $match[1] . $match[2] . $match[1]; | |
| 396 | 448 | |
| 397 | 449 | return $placeholder; |
| 398 | 450 | }; |
| 399 | 451 | |
| 400 | 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 | + * | |
| 401 | 458 | * The \\ messiness explained: |
| 402 | 459 | * * Don't count ' or " as end-of-string if it's escaped (has backslash |
| 403 | 460 | * in front of it) |
| 404 | 461 | * * Unless... that backslash itself is escaped (another leading slash), |
| @@ -408,9 +465,10 @@ | ||
| 408 | 465 | * to be done to pass the backslash into the PHP string without it being |
| 409 | 466 | * considered as escape-char (times 2) and to get it in the regex, |
| 410 | 467 | * escaped (times 2) |
| 411 | 468 | */ |
| 412 | - $this->registerPattern('/(['.$chars.'])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); | |
| 469 | + | |
| 470 | + $this->registerPattern('/([' . $chars . '])(.{0,65535}?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); | |
| 413 | 471 | } |
| 414 | 472 | |
| 415 | 473 | /** |
| 416 | 474 | * This method will restore all extracted data (strings, regexes) that were |
| @@ -446,16 +504,22 @@ | ||
| 446 | 504 | { |
| 447 | 505 | $parsed = parse_url($path); |
| 448 | 506 | if ( |
| 449 | 507 | // file is elsewhere |
| 450 | - isset($parsed['host']) || | |
| 508 | + isset($parsed['host']) | |
| 451 | 509 | // file responds to queries (may change, or need to bypass cache) |
| 452 | - isset($parsed['query']) | |
| 510 | + || isset($parsed['query']) | |
| 453 | 511 | ) { |
| 454 | 512 | return false; |
| 455 | 513 | } |
| 456 | 514 | |
| 457 | - return strlen($path) < PHP_MAXPATHLEN && @is_file($path) && is_readable($path); | |
| 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 | + } | |
| 458 | 522 | } |
| 459 | 523 | |
| 460 | 524 | /** |
| 461 | 525 | * Attempts to open file specified by $path for writing. |
| @@ -468,9 +532,9 @@ | ||
| 468 | 532 | */ |
| 469 | 533 | protected function openFileForWriting($path) |
| 470 | 534 | { |
| 471 | 535 | 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.'); | |
| 536 | + throw new IOException('The file "' . $path . '" could not be opened for writing. Check if PHP has enough permissions.'); | |
| 473 | 537 | } |
| 474 | 538 | |
| 475 | 539 | return $handler; |
| 476 | 540 | } |
| @@ -478,10 +542,10 @@ | ||
| 478 | 542 | /** |
| 479 | 543 | * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. |
| 480 | 544 | * |
| 481 | 545 | * @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) | |
| 546 | + * @param string $content The content to write | |
| 547 | + * @param string $path The path to the file (for exception printing only) | |
| 484 | 548 | * |
| 485 | 549 | * @throws IOException |
| 486 | 550 | */ |
| 487 | 551 | protected function writeToFile($handler, $content, $path = '') |
| @@ -486,12 +550,22 @@ | ||
| 486 | 550 | */ |
| 487 | 551 | protected function writeToFile($handler, $content, $path = '') |
| 488 | 552 | { |
| 489 | 553 | if ( |
| 490 | - !is_resource($handler) || | |
| 491 | - ($result = @fwrite($handler, $content)) === false || | |
| 492 | - ($result < strlen($content)) | |
| 554 | + !is_resource($handler) | |
| 555 | + || ($result = @fwrite($handler, $content)) === false | |
| 556 | + || ($result < strlen($content)) | |
| 493 | 557 | ) { |
| 494 | - throw new IOException('The file "'.$path.'" could not be written to. Check your disk space and file permissions.'); | |
| 558 | + throw new IOException('The file "' . $path . '" could not be written to. Check your disk space and file permissions.'); | |
| 495 | 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; | |
| 496 | 570 | } |
| 497 | 571 | } |