core/core.services.yml | 6 + core/includes/common.inc | 9 +- core/includes/theme.inc | 8 + .../Drupal/Core/Asset/JsCollectionOptimizer.php | 24 ++- .../Core/Asset/JsLicenseWebLabelsAnnotator.php | 53 ++++++ .../src/Controller/AssetLicenseInfoController.php | 182 +++++++++++++++++++++ .../system/src/Tests/Common/JavaScriptTest.php | 5 + core/modules/system/system.routing.yml | 8 + 8 files changed, 287 insertions(+), 8 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 8527b27..d43ae9e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -893,6 +893,12 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + asset.js.optimizer_license_web_labels_annotator: + class: Drupal\Core\Asset\JsLicenseWebLabelsAnnotator + arguments: [ '@url_generator' ] + asset.js.collection_optimizer_license_web_labels_annotator: + class: Drupal\Core\Asset\JsCollectionOptimizer + arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer_license_web_labels_annotator', '@asset.js.dumper', '@state', 'system.js_license_web_labels_files' ] library.discovery: class: Drupal\Core\Asset\LibraryDiscovery arguments: ['@library.discovery.collector'] diff --git a/core/includes/common.inc b/core/includes/common.inc index 6c6c4a3..00699f0 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -1994,7 +1994,14 @@ function drupal_pre_render_scripts($elements) { // Aggregate the JavaScript if necessary, but only during normal site // operation. if (!defined('MAINTENANCE_MODE') && \Drupal::config('system.performance')->get('js.preprocess')) { - $js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); + $optimized_js_assets = \Drupal::service('asset.js.collection_optimizer')->optimize($js_assets); + // Also create the alternative version of the aggregate that is unoptimized, + // but annotated with deep links to the license information on the + // JavaScript License Web Labels page. + // @see \Drupal\system\Controller\AssetLicenseInfoController + \Drupal::service('asset.js.collection_optimizer_license_web_labels_annotator')->optimize($js_assets); + // Now the optimized JavaScript assets are the ones to be rendered. + $js_assets = $optimized_js_assets; } return \Drupal::service('asset.js.collection_renderer')->render($js_assets); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index 8d985b3..b54d3c4 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2093,6 +2093,14 @@ function template_preprocess_html(&$variables) { } $variables['page_top'][] = array('#markup' => $page->getBodyTop()); $variables['page_bottom'][] = array('#markup' => $page->getBodyBottom()); + + // On each page that uses JavaScript, add the mandatory JavaScript Web License + // Labels link. + if (count(drupal_get_js('header')) || count(drupal_get_js('footer'))) { + $variables['page_bottom'][] = array( + '#markup' => l(t('JavaScript license information'), 'system/jslicense', array('attributes' => array('rel' => 'jslicense', 'class' => 'hidden'))), + ); + } } /** diff --git a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php index 2356479..99b27b5 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -8,7 +8,6 @@ use Drupal\Core\State\StateInterface; - /** * Optimizes JavaScript assets. */ @@ -43,6 +42,13 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $state; /** + * The name for the JavaScript asset collections optimized by this instance. + * + * @var string + */ + protected $name; + + /** * Constructs a JsCollectionOptimizer. * * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface @@ -53,12 +59,16 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { * The dumper for optimized JS assets. * @param \Drupal\Core\State\StateInterface * The state key/value store. + * @param string + * (optional) The name for the JavaScript asset collections optimized by + * this instance. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) { + public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, $name = 'system.js_cache_files') { $this->grouper = $grouper; $this->optimizer = $optimizer; $this->dumper = $dumper; $this->state = $state; + $this->name = $name; } /** @@ -85,7 +95,7 @@ public function optimize(array $js_assets) { // Drupal contrib can override this default JS aggregator to keep the same // grouping, optimizing and dumping, but change the strategy that is used to // determine when the aggregate should be rebuilt (e.g. mtime, HTTPS …). - $map = $this->state->get('system.js_cache_files') ?: array(); + $map = $this->state->get($this->name) ?: array(); $js_assets = array(); foreach ($js_groups as $order => $js_group) { // We have to return a single asset, not a group of assets. It is now up @@ -109,7 +119,7 @@ public function optimize(array $js_assets) { $uri = $map[$key]; } if (empty($uri) || !file_exists($uri)) { - // Concatenate each asset within the group. + // Concatenate and optimize each asset within the group. $data = ''; foreach ($js_group['items'] as $js_asset) { $data .= $this->optimizer->optimize($js_asset); @@ -123,7 +133,7 @@ public function optimize(array $js_assets) { $js_assets[$order]['data'] = $uri; // Persist the URI for this aggregate file. $map[$key] = $uri; - $this->state->set('system.js_cache_files', $map); + $this->state->set($this->name, $map); } else { // Use the persisted URI for the optimized JS file. @@ -168,14 +178,14 @@ protected function generateHash(array $js_group) { * {@inheritdoc} */ public function getAll() { - return $this->state->get('system.js_cache_files'); + return $this->state->get($this->name); } /** * {@inheritdoc} */ public function deleteAll() { - $this->state->delete('system.js_cache_files'); + $this->state->delete($this->name); $delete_stale = function($uri) { // Default stale file threshold is 30 days. if (REQUEST_TIME - filemtime($uri) > \Drupal::config('system.performance')->get('stale_file_threshold')) { diff --git a/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php b/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php new file mode 100644 index 0000000..a487b27 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/JsLicenseWebLabelsAnnotator.php @@ -0,0 +1,53 @@ +urlGenerator = $url_generator; + } + + /** + * {@inheritdoc} + */ + public function optimize(array $js_asset) { + if ($js_asset['type'] !== 'file') { + throw new \Exception('Only file JavaScript assets can be optimized.'); + } + if ($js_asset['type'] === 'file' && !$js_asset['preprocess']) { + throw new \Exception('Only file JavaScript assets with preprocessing enabled can be optimized.'); + } + + // Generate a prefix to be prepended to the "optimized" asset (no actual + // optimizations are made; this is a no-op optimizer) that deep-links to the + // license information on the JavaScript License Web Labels page. + $url = $this->urlGenerator->generateFromRoute('system.javascript_license_web_labels', array(), array('fragment' => drupal_clean_css_identifier($js_asset['data']))); + $prefix = "/** JavaScript asset: " . $js_asset['data'] . '; for license information, see ' . $url . " **/\n\n"; + return $prefix . file_get_contents($js_asset['data']); + } + +} diff --git a/core/modules/system/src/Controller/AssetLicenseInfoController.php b/core/modules/system/src/Controller/AssetLicenseInfoController.php new file mode 100644 index 0000000..0abeba9 --- /dev/null +++ b/core/modules/system/src/Controller/AssetLicenseInfoController.php @@ -0,0 +1,182 @@ +get('library.discovery'), + $container->get('asset.js.collection_optimizer'), + $container->get('asset.js.collection_optimizer_license_web_labels_annotator') + ); + } + + /** + * Constructs a AssetLicenseInfoController object. + * + * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery + * The asset library discovery service. + * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer + * The JavaScript asset collection optimizer service. + * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $js_collection_optimizer_license_annotator + * The JavaScript asset collection optimizer license annotator service. + */ + public function __construct(LibraryDiscoveryInterface $library_discovery, AssetCollectionOptimizerInterface $js_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer_license_annotator) { + $this->libraryDiscovery = $library_discovery; + $this->jsCollectionOptimizer = $js_collection_optimizer; + $this->jsCollectionOptimizerLicenseAnnotator = $js_collection_optimizer_license_annotator; + } + + /** + * Returns the JavaScript License Web Labels page for all JavaScript assets. + * + * @see http://www.gnu.org/licenses/javascript-labels.html + * + * @return array() + * A renderable array. + */ + public function jslicense() { + $rows = array(); + + // List all extensions' JavaScript assets. + $module_list = $this->moduleHandler()->getModuleList(); + $extensions = array_merge(array('core'), array_keys($module_list)); + foreach ($extensions as $extension) { + $libraries = $this->libraryDiscovery->getLibrariesByExtension($extension); + $rows_for_extension = array(); + foreach ($libraries as $library) { + $license_column = l($library['license']['name'], $library['license']['url']); + if (!isset($library['js'])) { + continue; + } + foreach ($library['js'] as $js_asset) { + // This is core's special "drupalSettings" JavaScript asset. + if ($js_asset['type'] === 'setting') { + continue; + } + else if ($js_asset['type'] === 'external') { + $url = $js_asset['data']; + } + else { + $url = file_create_url($js_asset['data']); + } + $js_asset_basename = basename($js_asset['data']); + $js_file_column = l('' . $js_asset_basename . '', $url, array('html' => TRUE)); + $source_column = l('' . $js_asset_basename . '', $url, array('html' => TRUE)); + $rows_for_extension[] = array( + 'data' => array($js_file_column, $license_column, $source_column), + 'id' => drupal_clean_css_identifier($js_asset['data']), + ); + } + } + + // Add header for the current extension; add all rows for this extension. + if (count($rows_for_extension)) { + if ($extension !== 'core') { + $rows[] = array( + array( + 'data' => $this->t('JavaScript files of the @extension-name @extension-type', array( + '@extension-name' => $module_list[$extension]->getName(), + '@extension-type' => $module_list[$extension]->getType(), + )), + 'header' => TRUE, + 'colspan' => 3, + ), + ); + } + foreach ($rows_for_extension as $row) { + $rows[] = $row; + } + } + } + + // List all aggregated (and optimized) JavaScript assets. + $rows[] = array( + array( + 'data' => $this->t('Aggregated & optimized (minified) JavaScript files'), + 'header' => TRUE, + 'colspan' => 3, + ), + ); + + $aggregated_js_assets = $this->jsCollectionOptimizer->getAll(); + if (count($aggregated_js_assets)) { + $unoptimized_aggregated_js_assets = $this->jsCollectionOptimizerLicenseAnnotator->getAll(); + $license_column = $this->t('Combination'); + foreach (array_keys($aggregated_js_assets) as $hash) { + $optimized_js = $aggregated_js_assets[$hash]; + $unoptimized_js = $unoptimized_aggregated_js_assets[$hash]; + $js_file_column = l('' . basename($optimized_js) . '', file_create_url($optimized_js), array('html' => TRUE)); + $source_column = l('' . basename($unoptimized_js) . '', file_create_url($unoptimized_js), array('html' => TRUE)); + $rows[] = array($js_file_column, $license_column, $source_column); + } + } + + $table = array( + '#type' => 'table', + '#header' => array( + 'javascript_asset' => array( + 'data' => $this->t('JavaScript file'), + ), + 'license' => array( + 'data' => $this->t('License'), + ), + 'source' => array( + 'data' => $this->t('Source code'), + 'class' => array(RESPONSIVE_PRIORITY_MEDIUM), + ), + + ), + '#sticky' => TRUE, + '#responsive' => TRUE, + '#rows' => $rows, + '#attributes' => array( + // This ID is required by the JavaScript License Web Labels standard; + // see http://www.gnu.org/licenses/javascript-labels.html. + 'id' => 'jslicense-labels1', + ), + ); + + return $table; + } + +} diff --git a/core/modules/system/src/Tests/Common/JavaScriptTest.php b/core/modules/system/src/Tests/Common/JavaScriptTest.php index 2f5dcae..eff099e 100644 --- a/core/modules/system/src/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/src/Tests/Common/JavaScriptTest.php @@ -32,6 +32,11 @@ class JavaScriptTest extends DrupalUnitTestBase { function setUp() { parent::setUp(); + + // Ensure the system.javascript_license_web_labels route is available. + $this->installSchema('system', array('router')); + \Drupal::service('router.builder')->rebuild(); + // There are dependencies in drupal_get_js() on the theme layer so we need // to initialize it. drupal_theme_initialize(); diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index ba2a46d..b476f41 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -418,3 +418,11 @@ system.admin_content: _title: 'Content' requirements: _permission: 'access administration pages' + +system.javascript_license_web_labels: + path: '/system/jslicense' + defaults: + _content: '\Drupal\system\Controller\AssetLicenseInfoController::jslicense' + _title: 'JavaScript license information' + requirements: + _access: 'TRUE'