JsDelivr.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <?php
  2. namespace Drupal\bootstrap\Backport\Plugin\Provider;
  3. /**
  4. * The "jsdelivr" CDN provider plugin.
  5. *
  6. * Note: this class is a backport from the 8.x-3.x code base.
  7. *
  8. * @see https://drupal-bootstrap.org/api/bootstrap/namespace/Drupal%21bootstrap%21Plugin%21Provider/8
  9. *
  10. * @ingroup plugins_provider
  11. */
  12. class JsDelivr extends ProviderBase {
  13. protected $pluginId = 'jsdelivr';
  14. /**
  15. * The base API URL.
  16. *
  17. * @var string
  18. */
  19. const BASE_API_URL = 'https://data.jsdelivr.com/v1/package/npm';
  20. /**
  21. * The base CDN URL.
  22. *
  23. * @var string
  24. */
  25. const BASE_CDN_URL = 'https://cdn.jsdelivr.net/npm';
  26. /**
  27. * A list of latest versions, keyed by NPM package name.
  28. *
  29. * @var string[]
  30. */
  31. protected $latestVersion = array();
  32. /**
  33. * A list of themes, keyed by NPM package name.
  34. *
  35. * @var array[]
  36. */
  37. protected $themes = array();
  38. /**
  39. * A list of versions, keyed by NPM package name.
  40. *
  41. * @var array[]
  42. */
  43. protected $versions = array();
  44. /**
  45. * {@inheritdoc}
  46. */
  47. public function getDescription() {
  48. return t('<p><a href="!jsdelivr" target="_blank">jsDelivr</a> is a free multi-CDN infrastructure that uses <a href="!maxcdn" target="_blank">MaxCDN</a>, <a href="!cloudflare" target="_blank">Cloudflare</a> and many others to combine their powers for the good of the open source community... <a href="!jsdelivr_about" target="_blank">read more</a></p>', array(
  49. '!jsdelivr' => 'https://www.jsdelivr.com',
  50. '!jsdelivr_about' => 'https://www.jsdelivr.com/about',
  51. '!maxcdn' => 'https://www.maxcdn.com',
  52. '!cloudflare' => 'https://www.cloudflare.com',
  53. ));
  54. }
  55. /**
  56. * {@inheritdoc}
  57. */
  58. public function getLabel() {
  59. return t('jsDelivr');
  60. }
  61. /**
  62. * {@inheritdoc}
  63. */
  64. protected function discoverCdnAssets($version, $theme = 'bootstrap') {
  65. $themes = $this->getCdnThemes($version);
  66. return isset($themes[$theme]) ? $themes[$theme] : array();
  67. }
  68. /**
  69. * {@inheritdoc}
  70. */
  71. public function getCdnThemes($version = NULL) {
  72. if (!isset($version)) {
  73. $version = $this->getCdnVersion();
  74. }
  75. if (!isset($this->themes[$version])) {
  76. $instance = $this;
  77. $this->themes[$version] = $this->cacheGet('themes.' . static::escapeDelimiter($version), array(), function ($themes) use ($version, $instance) {
  78. return $instance->getCdnThemePhp53Callback($themes, $version);
  79. });
  80. }
  81. return $this->themes[$version];
  82. }
  83. /**
  84. * Callback to get around PHP 5.3's limitation of automatic binding of $this.
  85. *
  86. * @see https://www.drupal.org/project/bootstrap/issues/3054809
  87. *
  88. * {@inheritdoc}
  89. */
  90. public function getCdnThemePhp53Callback($themes, $version) {
  91. foreach (array('bootstrap', 'bootswatch') as $package) {
  92. $mappedVersion = $this->mapVersion($version, $package);
  93. $files = $this->requestApiV1($package, $mappedVersion);
  94. $themes = $this->parseThemes($files, $package, $mappedVersion, $themes);
  95. }
  96. return $themes;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function getCdnVersions($package = 'bootstrap') {
  102. if (!isset($this->versions[$package])) {
  103. $instance = $this;
  104. $this->versions[$package] = $this->cacheGet("versions.$package", array(), function ($versions) use ($package, $instance) {
  105. return $instance->getCdnVersionsPhp53Callback($versions, $package);
  106. });
  107. }
  108. return $this->versions[$package];
  109. }
  110. /**
  111. * Callback to get around PHP 5.3's limitation of automatic binding of $this.
  112. *
  113. * @see https://www.drupal.org/project/bootstrap/issues/3054809
  114. *
  115. * {@inheritdoc}
  116. */
  117. public function getCdnVersionsPhp53Callback($versions, $package) {
  118. $json = $this->requestApiV1($package) + array('versions' => array());
  119. foreach ($json['versions'] as $version) {
  120. // Skip irrelevant versions.
  121. if (!preg_match('/^' . substr(BOOTSTRAP_VERSION, 0, 1) . '\.\d+\.\d+$/', $version)) {
  122. continue;
  123. }
  124. $versions[$version] = $version;
  125. }
  126. return $versions;
  127. }
  128. /**
  129. * {@inheritdoc}
  130. */
  131. protected function mapVersion($version, $package = NULL) {
  132. // While the Bootswatch project attempts to maintain version parity with
  133. // Bootstrap, it doesn't always happen. This causes issues when the system
  134. // expects a 1:1 version match between Bootstrap and Bootswatch.
  135. // @see https://github.com/thomaspark/bootswatch/issues/892#ref-issue-410070082
  136. if ($package === 'bootswatch') {
  137. switch ($version) {
  138. // This version is "broken" because of jsDelivr's API limit.
  139. case '3.4.1':
  140. $version = '3.4.0';
  141. break;
  142. // This version doesn't exist.
  143. case '3.1.1':
  144. $version = '3.2.0';
  145. break;
  146. }
  147. }
  148. return $version;
  149. }
  150. /**
  151. * Parses JSON from the API and retrieves valid files.
  152. *
  153. * @param array $json
  154. * The JSON data to parse.
  155. *
  156. * @return array
  157. * An array of files parsed from provided JSON data.
  158. */
  159. protected function parseFiles(array $json) {
  160. // Immediately return if malformed.
  161. if (!isset($json['files']) || !is_array($json['files'])) {
  162. return array();
  163. }
  164. $files = array();
  165. foreach ($json['files'] as $file) {
  166. // Skip old bootswatch file structure.
  167. if (preg_match('`^/2|/bower_components`', $file['name'], $matches)) {
  168. continue;
  169. }
  170. preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file['name'], $matches);
  171. if (!empty($matches[1]) && !empty($matches[4])) {
  172. $files[] = $file['name'];
  173. }
  174. }
  175. return $files;
  176. }
  177. /**
  178. * Extracts assets from files provided by the jsDelivr API.
  179. *
  180. * This will place the raw files into proper "css", "js" and "min" arrays
  181. * (if they exist) and prepends them with a base URL provided.
  182. *
  183. * @param array $files
  184. * An array of files to process.
  185. * @param string $package
  186. * The base URL each one of the $files are relative to, this usually
  187. * should also include the version path prefix as well.
  188. * @param string $version
  189. * A specific version to use.
  190. * @param array $themes
  191. * An existing array of themes. This is primarily used when building a
  192. * complete list of themes.
  193. *
  194. * @return array
  195. * An associative array containing the following keys, if there were
  196. * matching files found:
  197. * - css
  198. * - js
  199. * - min:
  200. * - css
  201. * - js
  202. */
  203. protected function parseThemes(array $files, $package, $version, array $themes = array()) {
  204. $baseUrl = static::BASE_CDN_URL . "/$package@$version";
  205. foreach ($files as $file) {
  206. preg_match('`([^/]*)/bootstrap(-theme)?(\.min)?\.(js|css)$`', $file, $matches);
  207. if (!empty($matches[1]) && !empty($matches[4])) {
  208. $path = $matches[1];
  209. $min = $matches[3];
  210. $filetype = $matches[4];
  211. // Determine the "theme" name.
  212. if ($path === 'css' || $path === 'js') {
  213. $theme = 'bootstrap';
  214. $title = (string) t('Bootstrap');
  215. }
  216. else {
  217. $theme = $path;
  218. $title = ucfirst($path);
  219. }
  220. if ($matches[2]) {
  221. $theme = 'bootstrap_theme';
  222. $title = (string) t('Bootstrap Theme');
  223. }
  224. $themes[$theme]['title'] = $title;
  225. if ($min) {
  226. $themes[$theme]['min'][$filetype][] = "$baseUrl/" . ltrim($file, '/');
  227. }
  228. else {
  229. $themes[$theme][$filetype][] = "$baseUrl/" . ltrim($file, '/');
  230. }
  231. }
  232. }
  233. // Post process the themes to fill in any missing assets.
  234. foreach (array_keys($themes) as $theme) {
  235. // Some themes do not have a non-minified version, clone them to the
  236. // "normal" css/js arrays to ensure that the theme still loads if
  237. // aggregation (minification) is disabled.
  238. foreach (array('css', 'js') as $type) {
  239. if (!isset($themes[$theme][$type]) && isset($themes[$theme]['min'][$type])) {
  240. $themes[$theme][$type] = $themes[$theme]['min'][$type];
  241. }
  242. }
  243. // Prepend the main Bootstrap styles before the Bootstrap theme.
  244. if ($theme === 'bootstrap_theme') {
  245. if (isset($themes['bootstrap']['css'])) {
  246. $themes[$theme]['css'] = array_unique(array_merge($themes['bootstrap']['css'], isset($themes[$theme]['css']) ? $themes[$theme]['css'] : array()));
  247. }
  248. if (isset($themes['bootstrap']['min']['css'])) {
  249. $themes[$theme]['min']['css'] = array_unique(array_merge($themes['bootstrap']['min']['css'], isset($themes[$theme]['min']['css']) ? $themes[$theme]['min']['css'] : array()));
  250. }
  251. }
  252. // Populate missing JavaScript.
  253. if (!isset($themes[$theme]['js']) && isset($themes['bootstrap']['js'])) {
  254. $themes[$theme]['js'] = $themes['bootstrap']['js'];
  255. }
  256. if (!isset($themes[$theme]['min']['js']) && isset($themes['bootstrap']['min']['js'])) {
  257. $themes[$theme]['min']['js'] = $themes['bootstrap']['min']['js'];
  258. }
  259. }
  260. return $themes;
  261. }
  262. /**
  263. * Requests JSON from jsDelivr's API V1.
  264. *
  265. * @param string $package
  266. * The NPM package being requested.
  267. * @param string $version
  268. * A specific version of $package to request. If not provided, a list of
  269. * available versions will be returned.
  270. *
  271. * @return array
  272. * The JSON data from the API.
  273. */
  274. protected function requestApiV1($package, $version = NULL) {
  275. $url = static::BASE_API_URL . "/$package";
  276. // If no version was passed, then all versions are returned.
  277. if (!$version) {
  278. return $this->requestJson($url);
  279. }
  280. $json = $this->requestJson("$url@$version/flat");
  281. // If bootstrap JSON could not be returned, provide defaults.
  282. if (!$json && $package === 'bootstrap') {
  283. return array(
  284. '/dist/css/bootstrap.css',
  285. '/dist/js/bootstrap.js',
  286. '/dist/css/bootstrap.min.css',
  287. '/dist/js/bootstrap.min.js',
  288. );
  289. }
  290. // Parse the files from JSON.
  291. return $this->parseFiles($json);
  292. }
  293. }