This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
Check for implicit conversion of array to boolean.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
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) { |
||
0 ignored issues
–
show
|
|||
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) { |
||
0 ignored issues
–
show
The expression
$this->extracted of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using
Loading history...
|
|||
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 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.