| @@ -12,8 +12,9 @@ | ||
| 12 | 12 | |
| 13 | 13 | namespace MatthiasMullie\Minify; |
| 14 | 14 | |
| 15 | 15 | use MatthiasMullie\Minify\Exceptions\IOException; |
| 16 | +use MatthiasMullie\Minify\Exceptions\PatternMatchException; | |
| 16 | 17 | use Psr\Cache\CacheItemInterface; |
| 17 | 18 | |
| 18 | 19 | /** |
| 19 | 20 | * Abstract minifier class. |
| @@ -229,9 +230,9 @@ | ||
| 229 | 230 | /** |
| 230 | 231 | * Save to file. |
| 231 | 232 | * |
| 232 | 233 | * @param string $content The minified data |
| 233 | - * @param string $path The path to save the minified data to | |
| 234 | + * @param string $path The path to save the minified data to | |
| 234 | 235 | * |
| 235 | 236 | * @throws IOException |
| 236 | 237 | */ |
| 237 | 238 | protected function save($content, $path) |
| @@ -248,9 +249,9 @@ | ||
| 248 | 249 | * |
| 249 | 250 | * If $replacement is a string, it must be plain text. Placeholders like $1 or \2 don't work. |
| 250 | 251 | * If you need that functionality, use a callback instead. |
| 251 | 252 | * |
| 252 | - * @param string $pattern PCRE pattern | |
| 253 | + * @param string $pattern PCRE pattern | |
| 253 | 254 | * @param string|callable $replacement Replacement value for matched pattern |
| 254 | 255 | */ |
| 255 | 256 | protected function registerPattern($pattern, $replacement = '') |
| 256 | 257 | { |
| @@ -264,25 +265,11 @@ | ||
| 264 | 265 | * Both JS and CSS use the same form of multi-line comment, so putting the common code here. |
| 265 | 266 | */ |
| 266 | 267 | protected function stripMultilineComments() |
| 267 | 268 | { |
| 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 | 269 | $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 | - | |
| 270 | + // Pattern for matching comments that we want to preserve | |
| 271 | + $keepPattern = '/^ | |
| 285 | 272 | # comment content |
| 286 | 273 | (?: |
| 287 | 274 | # either starts with an ! |
| 288 | 275 | ! |
| @@ -292,16 +279,26 @@ | ||
| 292 | 279 | |
| 293 | 280 | # there is either a @license or @preserve tag |
| 294 | 281 | @(?:license|preserve) |
| 295 | 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 | 296 | |
| 297 | - # then match to the end of the comment | |
| 298 | - .*?\*\/\n? | |
| 297 | + return $placeholder; | |
| 298 | + }; | |
| 299 | 299 | |
| 300 | - /ixs', $callback); | |
| 301 | - | |
| 302 | - // Then strip all other comments | |
| 303 | - $this->registerPattern('/\/\*.*?\*\//s', ''); | |
| 300 | + $this->registerPattern('/\n?\/\*(.*?)\*\/\n?/s', $callback); | |
| 304 | 301 | } |
| 305 | 302 | |
| 306 | 303 | /** |
| 307 | 304 | * We can't "just" run some regular expressions against JavaScript: it's a |
| @@ -313,8 +310,10 @@ | ||
| 313 | 310 | * |
| 314 | 311 | * @param string $content The content to replace patterns in |
| 315 | 312 | * |
| 316 | 313 | * @return string The (manipulated) content |
| 314 | + * | |
| 315 | + * @throws PatternMatchException | |
| 317 | 316 | */ |
| 318 | 317 | protected function replace($content) |
| 319 | 318 | { |
| 320 | 319 | $contentLength = strlen($content); |
| @@ -340,9 +339,10 @@ | ||
| 340 | 339 | continue; |
| 341 | 340 | } |
| 342 | 341 | |
| 343 | 342 | $match = null; |
| 344 | - if (preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset)) { | |
| 343 | + $matchResult = preg_match($pattern, $content, $match, PREG_OFFSET_CAPTURE, $processedOffset); | |
| 344 | + if ($matchResult) { | |
| 345 | 345 | $matches[$i] = $match; |
| 346 | 346 | |
| 347 | 347 | // we'll store the match position as well; that way, we |
| 348 | 348 | // don't have to redo all preg_matches after changing only |
| @@ -348,8 +348,13 @@ | ||
| 348 | 348 | // don't have to redo all preg_matches after changing only |
| 349 | 349 | // the first (we'll still know where those others are) |
| 350 | 350 | $positions[$i] = $match[0][1]; |
| 351 | 351 | } else { |
| 352 | + if ($matchResult === false) { | |
| 353 | + throw PatternMatchException::fromLastError( | |
| 354 | + "Failed to match pattern '$pattern' at $processedOffset" | |
| 355 | + ); | |
| 356 | + } | |
| 352 | 357 | // if the pattern couldn't be matched, there's no point in |
| 353 | 358 | // executing it again in later runs on this same content; |
| 354 | 359 | // ignore this one until we reach end of content |
| 355 | 360 | unset($matches[$i], $positions[$i]); |
| @@ -389,9 +394,9 @@ | ||
| 389 | 394 | * If $replacement is a callback, execute it, passing in the match data. |
| 390 | 395 | * If it's a string, just pass it through. |
| 391 | 396 | * |
| 392 | 397 | * @param string|callable $replacement Replacement value |
| 393 | - * @param array $match Match data, in PREG_OFFSET_CAPTURE form | |
| 398 | + * @param array $match Match data, in PREG_OFFSET_CAPTURE form | |
| 394 | 399 | * |
| 395 | 400 | * @return string |
| 396 | 401 | */ |
| 397 | 402 | protected function executeReplacement($replacement, $match) |
| @@ -444,8 +449,13 @@ | ||
| 444 | 449 | return $placeholder; |
| 445 | 450 | }; |
| 446 | 451 | |
| 447 | 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 | + * | |
| 448 | 458 | * The \\ messiness explained: |
| 449 | 459 | * * Don't count ' or " as end-of-string if it's escaped (has backslash |
| 450 | 460 | * in front of it) |
| 451 | 461 | * * Unless... that backslash itself is escaped (another leading slash), |
| @@ -455,9 +465,10 @@ | ||
| 455 | 465 | * to be done to pass the backslash into the PHP string without it being |
| 456 | 466 | * considered as escape-char (times 2) and to get it in the regex, |
| 457 | 467 | * escaped (times 2) |
| 458 | 468 | */ |
| 459 | - $this->registerPattern('/([' . $chars . '])(.*?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); | |
| 469 | + | |
| 470 | + $this->registerPattern('/([' . $chars . '])(.{0,65535}?(?<!\\\\)(\\\\\\\\)*+)\\1/s', $callback); | |
| 460 | 471 | } |
| 461 | 472 | |
| 462 | 473 | /** |
| 463 | 474 | * This method will restore all extracted data (strings, regexes) that were |
| @@ -531,10 +542,10 @@ | ||
| 531 | 542 | /** |
| 532 | 543 | * Attempts to write $content to the file specified by $handler. $path is used for printing exceptions. |
| 533 | 544 | * |
| 534 | 545 | * @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) | |
| 546 | + * @param string $content The content to write | |
| 547 | + * @param string $path The path to the file (for exception printing only) | |
| 537 | 548 | * |
| 538 | 549 | * @throws IOException |
| 539 | 550 | */ |
| 540 | 551 | protected function writeToFile($handler, $content, $path = '') |