PageRenderTime 41ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/minify/matthiasmullie-minify/src/Minify.php

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