Pipeline.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  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\Traits\AssetUtilsTrait;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Filesystem\Folder;
  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://');
  77. if (!$this->assets_dir) {
  78. // Attempt to create assets folder if it doesn't exist yet.
  79. $this->assets_dir = $locator->findResource('asset://', true, true);
  80. Folder::mkdir($this->assets_dir);
  81. $locator->clearCache();
  82. }
  83. $this->assets_url = $locator->findResource('asset://', false);
  84. }
  85. /**
  86. * Minify and concatenate CSS
  87. *
  88. * @param array $assets
  89. * @param string $group
  90. * @param array $attributes
  91. * @return bool|string URL or generated content if available, else false
  92. */
  93. public function renderCss($assets, $group, $attributes = [])
  94. {
  95. // temporary list of assets to pipeline
  96. $inline_group = false;
  97. if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
  98. $inline_group = true;
  99. unset($attributes['loading']);
  100. }
  101. // Store Attributes
  102. $this->attributes = array_merge(['type' => 'text/css', 'rel' => 'stylesheet'], $attributes);
  103. // Compute uid based on assets and timestamp
  104. $json_assets = json_encode($assets);
  105. $uid = md5($json_assets . $this->css_minify . $this->css_rewrite . $group);
  106. $file = $uid . '.css';
  107. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  108. $filepath = "{$this->assets_dir}/{$file}";
  109. if (file_exists($filepath)) {
  110. $buffer = file_get_contents($filepath) . "\n";
  111. } else {
  112. //if nothing found get out of here!
  113. if (empty($assets)) {
  114. return false;
  115. }
  116. // Concatenate files
  117. $buffer = $this->gatherLinks($assets, self::CSS_ASSET);
  118. // Minify if required
  119. if ($this->shouldMinify('css')) {
  120. $minifier = new CSS();
  121. $minifier->add($buffer);
  122. $buffer = $minifier->minify();
  123. }
  124. // Write file
  125. if (trim($buffer) !== '') {
  126. file_put_contents($filepath, $buffer);
  127. }
  128. }
  129. if ($inline_group) {
  130. $output = "<style>\n" . $buffer . "\n</style>\n";
  131. } else {
  132. $this->asset = $relative_path;
  133. $output = '<link href="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . ">\n";
  134. }
  135. return $output;
  136. }
  137. /**
  138. * Minify and concatenate JS files.
  139. *
  140. * @param array $assets
  141. * @param string $group
  142. * @param array $attributes
  143. * @return bool|string URL or generated content if available, else false
  144. */
  145. public function renderJs($assets, $group, $attributes = [])
  146. {
  147. // temporary list of assets to pipeline
  148. $inline_group = false;
  149. if (array_key_exists('loading', $attributes) && $attributes['loading'] === 'inline') {
  150. $inline_group = true;
  151. unset($attributes['loading']);
  152. }
  153. // Store Attributes
  154. $this->attributes = $attributes;
  155. // Compute uid based on assets and timestamp
  156. $json_assets = json_encode($assets);
  157. $uid = md5($json_assets . $this->js_minify . $group);
  158. $file = $uid . '.js';
  159. $relative_path = "{$this->base_url}{$this->assets_url}/{$file}";
  160. $filepath = "{$this->assets_dir}/{$file}";
  161. if (file_exists($filepath)) {
  162. $buffer = file_get_contents($filepath) . "\n";
  163. } else {
  164. //if nothing found get out of here!
  165. if (empty($assets)) {
  166. return false;
  167. }
  168. // Concatenate files
  169. $buffer = $this->gatherLinks($assets, self::JS_ASSET);
  170. // Minify if required
  171. if ($this->shouldMinify('js')) {
  172. $minifier = new JS();
  173. $minifier->add($buffer);
  174. $buffer = $minifier->minify();
  175. }
  176. // Write file
  177. if (trim($buffer) !== '') {
  178. file_put_contents($filepath, $buffer);
  179. }
  180. }
  181. if ($inline_group) {
  182. $output = '<script' . $this->renderAttributes(). ">\n" . $buffer . "\n</script>\n";
  183. } else {
  184. $this->asset = $relative_path;
  185. $output = '<script src="' . $relative_path . $this->renderQueryString() . '"' . $this->renderAttributes() . BaseAsset::integrityHash($this->asset) . "></script>\n";
  186. }
  187. return $output;
  188. }
  189. /**
  190. * Finds relative CSS urls() and rewrites the URL with an absolute one
  191. *
  192. * @param string $file the css source file
  193. * @param string $dir , $local relative path to the css file
  194. * @param bool $local is this a local or remote asset
  195. * @return string
  196. */
  197. protected function cssRewrite($file, $dir, $local)
  198. {
  199. // Strip any sourcemap comments
  200. $file = preg_replace(self::CSS_SOURCEMAP_REGEX, '', $file);
  201. // Find any css url() elements, grab the URLs and calculate an absolute path
  202. // Then replace the old url with the new one
  203. $file = (string)preg_replace_callback(self::CSS_URL_REGEX, function ($matches) use ($dir, $local) {
  204. $old_url = $matches[2];
  205. // Ensure link is not rooted to web server, a data URL, or to a remote host
  206. if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url) || Utils::startsWith($old_url, 'data:') || $this->isRemoteLink($old_url)) {
  207. return $matches[0];
  208. }
  209. // clean leading /
  210. $old_url = Utils::normalizePath($dir . '/' . $old_url);
  211. if (preg_match(self::FIRST_FORWARDSLASH_REGEX, $old_url)) {
  212. $old_url = ltrim($old_url, '/');
  213. }
  214. $new_url = ($local ? $this->base_url : '') . $old_url;
  215. return str_replace($matches[2], $new_url, $matches[0]);
  216. }, $file);
  217. return $file;
  218. }
  219. /**
  220. * @param string $type
  221. * @return bool
  222. */
  223. private function shouldMinify($type = 'css')
  224. {
  225. $check = $type . '_minify';
  226. $win_check = $type . '_minify_windows';
  227. $minify = (bool) $this->$check;
  228. // If this is a Windows server, and minify_windows is false (default value) skip the
  229. // minification process because it will cause Apache to die/crash due to insufficient
  230. // ThreadStackSize in httpd.conf - See: https://bugs.php.net/bug.php?id=47689
  231. if (stripos(php_uname('s'), 'WIN') === 0 && !$this->{$win_check}) {
  232. $minify = false;
  233. }
  234. return $minify;
  235. }
  236. }