Pipeline.php 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. <?php
  2. /**
  3. * @package Grav\Common\Assets
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Assets;
  9. use Grav\Common\Assets\Traits\AssetUtilsTrait;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Grav;
  12. use Grav\Common\Uri;
  13. use Grav\Common\Utils;
  14. use Grav\Framework\Object\PropertyObject;
  15. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  16. class Pipeline extends PropertyObject
  17. {
  18. use AssetUtilsTrait;
  19. protected const CSS_ASSET = true;
  20. protected const JS_ASSET = false;
  21. /** @const Regex to match CSS urls */
  22. protected const CSS_URL_REGEX = '{url\(([\'\"]?)(.*?)\1\)}';
  23. /** @const Regex to match CSS sourcemap comments */
  24. protected const CSS_SOURCEMAP_REGEX = '{\/\*# (.*?) \*\/}';
  25. /** @const Regex to match CSS import content */
  26. protected const CSS_IMPORT_REGEX = '{@import(.*?);}';
  27. protected const FIRST_FORWARDSLASH_REGEX = '{^\/{1}\w}';
  28. protected $css_minify;
  29. protected $css_minify_windows;
  30. protected $css_rewrite;
  31. protected $js_minify;
  32. protected $js_minify_windows;
  33. protected $base_url;
  34. protected $assets_dir;
  35. protected $assets_url;
  36. protected $timestamp;
  37. protected $attributes;
  38. protected $query;
  39. protected $asset;
  40. /**
  41. * Closure used by the pipeline to fetch assets.
  42. *
  43. * Useful when file_get_contents() function is not available in your PHP
  44. * installation or when you want to apply any kind of preprocessing to
  45. * your assets before they get pipelined.
  46. *
  47. * The closure will receive as the only parameter a string with the path/URL of the asset and
  48. * it should return the content of the asset file as a string.
  49. *
  50. * @var \Closure
  51. */
  52. protected $fetch_command;
  53. public function __construct(array $elements = [], ?string $key = null)
  54. {
  55. parent::__construct($elements, $key);
  56. /** @var UniformResourceLocator $locator */
  57. $locator = Grav::instance()['locator'];
  58. /** @var Config $config */
  59. $config = Grav::instance()['config'];
  60. /** @var Uri $uri */
  61. $uri = Grav::instance()['uri'];
  62. $this->base_url = rtrim($uri->rootUrl($config->get('system.absolute_urls')), '/') . '/';
  63. $this->assets_dir = $locator->findResource('asset://') . DS;
  64. $this->assets_url = $locator->findResource('asset://', false);
  65. }
  66. /**
  67. * Minify and concatenate CSS
  68. *
  69. * @param array $assets
  70. * @param string $group
  71. * @param array $attributes
  72. *
  73. * @return bool|string URL or generated content if available, else false
  74. */
  75. public function renderCss($assets, $group, $attributes = [])
  76. {
  77. // temporary list of assets to pipeline
  78. $inline_group = false;
  79. if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
  80. $inline_group = true;
  81. unset($attributes['loading']);
  82. }
  83. // Store Attributes
  84. $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes);
  85. // Compute uid based on assets and timestamp
  86. $json_assets = json_encode($assets);
  87. $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
  88. $file = $uid . '.css';
  89. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  90. $buffer = null;
  91. if (file_exists($this->assets_dir . $file)) {
  92. $buffer = file_get_contents($this->assets_dir . $file) . "\n";
  93. } else {
  94. //if nothing found get out of here!
  95. if (empty($assets)) {
  96. return false;
  97. }
  98. // Concatenate files
  99. $buffer = $this->gatherLinks($assets, self::CSS_ASSET);
  100. // Minify if required
  101. if ($this->shouldMinify('css')) {
  102. $minifier = new \MatthiasMullie\Minify\CSS();
  103. $minifier->add($buffer);
  104. $buffer = $minifier->minify();
  105. }
  106. // Write file
  107. if (trim($buffer) !== '') {
  108. file_put_contents($this->assets_dir . $file, $buffer);
  109. }
  110. }
  111. if ($inline_group) {
  112. $output = "<style>\n" . $buffer . "\n</style>\n";
  113. } else {
  114. $this->asset = $relative_path;
  115. $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . ">\n";
  116. }
  117. return $output;
  118. }
  119. /**
  120. * Minify and concatenate JS files.
  121. *
  122. * @param array $assets
  123. * @param string $group
  124. * @param array $attributes
  125. *
  126. * @return bool|string URL or generated content if available, else false
  127. */
  128. public function renderJs($assets, $group, $attributes = [])
  129. {
  130. // temporary list of assets to pipeline
  131. $inline_group = false;
  132. if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
  133. $inline_group = true;
  134. unset($attributes['loading']);
  135. }
  136. // Store Attributes
  137. $this->attributes = $attributes;
  138. // Compute uid based on assets and timestamp
  139. $json_assets = json_encode($assets);
  140. $uid = md5($json_assets . $this->js_minify . $group);
  141. $file = $uid . '.js';
  142. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  143. $buffer = null;
  144. if (file_exists($this->assets_dir . $file)) {
  145. $buffer = file_get_contents($this->assets_dir . $file) . "\n";
  146. } else {
  147. //if nothing found get out of here!
  148. if (empty($assets)) {
  149. return false;
  150. }
  151. // Concatenate files
  152. $buffer = $this->gatherLinks($assets, self::JS_ASSET);
  153. // Minify if required
  154. if ($this->shouldMinify('js')) {
  155. $minifier = new \MatthiasMullie\Minify\JS();
  156. $minifier->add($buffer);
  157. $buffer = $minifier->minify();
  158. }
  159. // Write file
  160. if (trim($buffer) !== '') {
  161. file_put_contents($this->assets_dir . $file, $buffer);
  162. }
  163. }
  164. if ($inline_group) {
  165. $output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
  166. } else {
  167. $this->asset = $relative_path;
  168. $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . "></script>\n";
  169. }
  170. return $output;
  171. }
  172. /**
  173. * Finds relative CSS urls() and rewrites the URL with an absolute one
  174. *
  175. * @param string $file the css source file
  176. * @param string $dir , $local relative path to the css file
  177. * @param bool $local is this a local or remote asset
  178. *
  179. * @return mixed
  180. */
  181. protected function cssRewrite($file, $dir, $local)
  182. {
  183. // Strip any sourcemap comments
  184. $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
  185. // Find any css url() elements, grab the URLs and calculate an absolute path
  186. // Then replace the old url with the new one
  187. $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) {
  188. $old_url = $matches[2];
  189. // Ensure link is not rooted to web server, a data URL, or to a remote host
  190. if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
  191. return $matches[0];
  192. }
  193. // clean leading /
  194. $old_url = Utils::normalizePath($dir . '/' . $old_url);
  195. if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
  196. $old_url = ltrim($old_url, '/');
  197. }
  198. $new_url = ($local ? $this->base_url: '') . $old_url;
  199. $fixed = str_replace($matches[2], $new_url, $matches[0]);
  200. return $fixed;
  201. }, $file);
  202. return $file;
  203. }
  204. private function shouldMinify($type = 'css')
  205. {
  206. $check = $type . '_minify';
  207. $win_check = $type . '_minify_windows';
  208. $minify = (bool) $this->$check;
  209. // If this is a Windows server, and minify_windows is false (default value) skip the
  210. // minification process because it will cause Apache to die/crash due to insufficient
  211. // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
  212. if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) {
  213. $minify = false;
  214. }
  215. return $minify;
  216. }
  217. }