AssetUtilsTrait.php 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. <?php
  2. /**
  3. * @package Grav\Common\Assets\Traits
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\Assets\Traits;
  9. use Closure;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Utils;
  12. use function dirname;
  13. use function in_array;
  14. use function is_array;
  15. /**
  16. * Trait AssetUtilsTrait
  17. * @package Grav\Common\Assets\Traits
  18. */
  19. trait AssetUtilsTrait
  20. {
  21. /**
  22. * @var Closure|null
  23. *
  24. * Closure used by the pipeline to fetch assets.
  25. *
  26. * Useful when file_get_contents() function is not available in your PHP
  27. * installation or when you want to apply any kind of preprocessing to
  28. * your assets before they get pipelined.
  29. *
  30. * The closure will receive as the only parameter a string with the path/URL of the asset and
  31. * it should return the content of the asset file as a string.
  32. */
  33. protected $fetch_command;
  34. /** @var string */
  35. protected $base_url;
  36. /**
  37. * Determine whether a link is local or remote.
  38. * Understands both "http://" and "https://" as well as protocol agnostic links "//"
  39. *
  40. * @param string $link
  41. * @return bool
  42. */
  43. public static function isRemoteLink($link)
  44. {
  45. $base = Grav::instance()['uri']->rootUrl(true);
  46. // Sanity check for local URLs with absolute URL's enabled
  47. if (Utils::startsWith($link, $base)) {
  48. return false;
  49. }
  50. return (0 === strpos($link, 'http://') || 0 === strpos($link, 'https://') || 0 === strpos($link, '//'));
  51. }
  52. /**
  53. * Download and concatenate the content of several links.
  54. *
  55. * @param array $assets
  56. * @param int $type
  57. * @return string
  58. */
  59. protected function gatherLinks(array $assets, int $type = self::CSS_ASSET): string
  60. {
  61. $buffer = '';
  62. foreach ($assets as $asset) {
  63. $local = true;
  64. $link = $asset->getAsset();
  65. $relative_path = $link;
  66. if (static::isRemoteLink($link)) {
  67. $local = false;
  68. if (0 === strpos($link, '//')) {
  69. $link = 'http:' . $link;
  70. }
  71. $relative_dir = dirname($relative_path);
  72. } else {
  73. // Fix to remove relative dir if grav is in one
  74. if (($this->base_url !== '/') && Utils::startsWith($relative_path, $this->base_url)) {
  75. $base_url = '#' . preg_quote($this->base_url, '#') . '#';
  76. $relative_path = ltrim(preg_replace($base_url, '/', $link, 1), '/');
  77. }
  78. $relative_dir = dirname($relative_path);
  79. $link = GRAV_ROOT . '/' . $relative_path;
  80. }
  81. // TODO: looks like this is not being used.
  82. $file = $this->fetch_command instanceof Closure ? @$this->fetch_command->__invoke($link) : @file_get_contents($link);
  83. // No file found, skip it...
  84. if ($file === false) {
  85. continue;
  86. }
  87. // Double check last character being
  88. if ($type === self::JS_ASSET || $type === self::JS_MODULE_ASSET) {
  89. $file = rtrim($file, ' ;') . ';';
  90. }
  91. // If this is CSS + the file is local + rewrite enabled
  92. if ($type === self::CSS_ASSET && $this->css_rewrite) {
  93. $file = $this->cssRewrite($file, $relative_dir, $local);
  94. }
  95. if ($type === self::JS_MODULE_ASSET) {
  96. $file = $this->jsRewrite($file, $relative_dir, $local);
  97. }
  98. $file = rtrim($file) . PHP_EOL;
  99. $buffer .= $file;
  100. }
  101. // Pull out @imports and move to top
  102. if ($type === self::CSS_ASSET) {
  103. $buffer = $this->moveImports($buffer);
  104. }
  105. return $buffer;
  106. }
  107. /**
  108. * Moves @import statements to the top of the file per the CSS specification
  109. *
  110. * @param string $file the file containing the combined CSS files
  111. * @return string the modified file with any @imports at the top of the file
  112. */
  113. protected function moveImports($file)
  114. {
  115. $regex = '{@import.*?["\']([^"\']+)["\'].*?;}';
  116. $imports = [];
  117. $file = (string)preg_replace_callback($regex, static function ($matches) use (&$imports) {
  118. $imports[] = $matches[0];
  119. return '';
  120. }, $file);
  121. return implode("\n", $imports) . "\n\n" . $file;
  122. }
  123. /**
  124. *
  125. * Build an HTML attribute string from an array.
  126. *
  127. * @return string
  128. */
  129. protected function renderAttributes()
  130. {
  131. $html = '';
  132. $no_key = ['loading'];
  133. foreach ($this->attributes as $key => $value) {
  134. if ($value === null) {
  135. continue;
  136. }
  137. if (is_numeric($key)) {
  138. $key = $value;
  139. }
  140. if (is_array($value)) {
  141. $value = implode(' ', $value);
  142. }
  143. if (in_array($key, $no_key, true)) {
  144. $element = htmlentities($value, ENT_QUOTES, 'UTF-8', false);
  145. } else {
  146. $element = $key . '="' . htmlentities($value, ENT_QUOTES, 'UTF-8', false) . '"';
  147. }
  148. $html .= ' ' . $element;
  149. }
  150. return $html;
  151. }
  152. /**
  153. * Render Querystring
  154. *
  155. * @param string|null $asset
  156. * @return string
  157. */
  158. protected function renderQueryString($asset = null)
  159. {
  160. $querystring = '';
  161. $asset = $asset ?? $this->asset;
  162. $attributes = $this->attributes;
  163. if (!empty($this->query)) {
  164. if (Utils::contains($asset, '?')) {
  165. $querystring .= '&' . $this->query;
  166. } else {
  167. $querystring .= '?' . $this->query;
  168. }
  169. }
  170. if ($this->timestamp) {
  171. if ($querystring || Utils::contains($asset, '?')) {
  172. $querystring .= '&' . $this->timestamp;
  173. } else {
  174. $querystring .= '?' . $this->timestamp;
  175. }
  176. }
  177. return $querystring;
  178. }
  179. }