Pipeline.php 8.7 KB

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