core/core.libraries.yml | 166 ++++++++++++++++----- core/core.services.yml | 2 +- core/includes/common.inc | 3 +- core/includes/theme.inc | 8 + .../Drupal/Core/Asset/JsCollectionOptimizer.php | 39 ++++- core/lib/Drupal/Core/Asset/LibraryDiscovery.php | 18 ++- .../Controller/AssetLicenseInfoController.php | 158 ++++++++++++++++++++ .../Drupal/system/Tests/Common/JavaScriptTest.php | 11 +- core/modules/system/system.routing.yml | 8 + .../Tests/Core/Asset/LibraryDiscoveryTest.php | 67 ++++++++- .../library_test_files/licenses.libraries.yml | 72 +++++++++ 11 files changed, 504 insertions(+), 48 deletions(-) diff --git a/core/core.libraries.yml b/core/core.libraries.yml index fb13d9c..6b0caab 100644 --- a/core/core.libraries.yml +++ b/core/core.libraries.yml @@ -3,6 +3,10 @@ backbone: remote: https://github.com/jashkenas/backbone version: 1.1.0 + license: + name: MIT + remote: https://github.com/jashkenas/backbone/blob/1.1.0/LICENSE + gpl-compatible: true js: assets/vendor/backbone/backbone.js: { weight: -19 } dependencies: @@ -12,6 +16,10 @@ classList: remote: https://github.com/eligrey/classList.js # @todo Stable release required for Drupal 8.0. version: master + license: + name: Public Domain + remote: https://github.com/eligrey/classList.js/blob/master/LICENSE.md + gpl-compatible: true js: assets/vendor/classList/classList.min.js: { weight: -21, browsers: { IE: 'lte IE 9', '!IE': false } } @@ -20,12 +28,20 @@ ckeditor: # @todo Stable release required for Drupal 8.0. version: 4.2-dev commit: 887d81ac1824008b690e439a1b29eb4f13b51212 + license: + name: GNU-GPL-2.0-or-later + remote: https://github.com/ckeditor/ckeditor-dev/blob/887d81ac1824008b690e439a1b29eb4f13b51212/LICENSE.md + gpl-compatible: true js: assets/vendor/ckeditor/ckeditor.js: { preprocess: false } domready: remote: https://github.com/ded/domready version: 1.0.4 + license: + name: MIT + remote: https://github.com/ded/domready/blob/v1.0.4/LICENSE + gpl-compatible: true js: assets/vendor/domready/ready.min.js: { weight: -21 } @@ -279,18 +295,30 @@ drupal.vertical-tabs: html5shiv: remote: https://github.com/aFarkas/html5shiv version: 3.6.2 + license: + name: GPL2 + remote: http://www.gnu.org/licenses/gpl-2.0.html + gpl-compatible: true js: assets/vendor/html5shiv/html5.js: { every_page: true, weight: -22, browsers: { IE: 'lte IE 8', '!IE': false } } jquery: remote: https://github.com/jquery/jquery version: 2.1.0 + license: + name: MIT + remote: https://github.com/jquery/jquery/blob/2.1.0/MIT-LICENSE.txt + gpl-compatible: true js: assets/vendor/jquery/jquery.js: { weight: -20 } jquery.cookie: remote: https://github.com/carhartl/jquery-cookie version: v1.4.0 + license: + name: MIT + remote: https://github.com/carhartl/jquery-cookie/blob/v1.4.0/MIT-LICENSE.txt + gpl-compatible: true js: assets/vendor/jquery.cookie/jquery.cookie.js: {} dependencies: @@ -300,6 +328,10 @@ jquery.farbtastic: remote: https://github.com/mattfarina/farbtastic # @todo Ping @robloach or @mattfarina to retroactively create this release. version: 1.2 + license: + name: GNU-GPL-2.0-or-later + remote: https://github.com/mattfarina/farbtastic/blob/master/LICENSE.txt + gpl-compatible: true js: assets/vendor/farbtastic/farbtastic.js: {} css: @@ -311,6 +343,10 @@ jquery.farbtastic: jquery.form: remote: https://github.com/malsup/form version: 3.50 + license: + name: GNU-GPL-2.0-or-later + remote: http://malsup.github.com/gpl-license-v2.txt + gpl-compatible: true js: assets/vendor/jquery-form/jquery.form.js: {} dependencies: @@ -326,6 +362,10 @@ jquery.intrinsic: jquery.joyride: remote: https://github.com/zurb/joyride version: v2.0.3 + license: + name: MIT + remote: https://github.com/zurb/joyride/blob/v2.0.3/README.markdown + gpl-compatible: true js: assets/vendor/jquery-joyride/jquery.joyride-2.0.3.js: {} dependencies: @@ -335,6 +375,10 @@ jquery.joyride: jquery.once: remote: https://github.com/RobLoach/jquery-once version: 1.2.3 + license: + name: GNU-GPL-2.0-or-later + remote: https://github.com/RobLoach/jquery-once/blob/1.2.3/LICENSE + gpl-compatible: true js: assets/vendor/jquery-once/jquery.once.js: { weight: -19 } dependencies: @@ -342,7 +386,11 @@ jquery.once: jquery.ui: remote: https://github.com/jquery/jquery-ui - version: &jquery_ui 1.10.2 + version: &jquery_ui_version 1.10.2 + license: &jquery_ui_license + name: MIT + remote: https://github.com/jquery/jquery-ui/blob/1.10.2/MIT-LICENSE.txt + gpl-compatible: true js: assets/vendor/jquery.ui/ui/jquery.ui.core.js: { weight: -11 } css: @@ -354,7 +402,8 @@ jquery.ui: - core/jquery jquery.ui.accordion: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.accordion.js: {} css: @@ -365,7 +414,8 @@ jquery.ui.accordion: - core/jquery.ui.widget jquery.ui.autocomplete: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.autocomplete.js: {} css: @@ -378,7 +428,8 @@ jquery.ui.autocomplete: - core/jquery.ui.menu jquery.ui.button: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.button.js: {} css: @@ -389,7 +440,8 @@ jquery.ui.button: - core/jquery.ui.widget jquery.ui.datepicker: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.datepicker.js: {} css: @@ -399,7 +451,8 @@ jquery.ui.datepicker: - core/jquery.ui jquery.ui.dialog: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.dialog.js: {} css: @@ -415,7 +468,8 @@ jquery.ui.dialog: - core/jquery.ui.resizable jquery.ui.draggable: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.draggable.js: {} dependencies: @@ -424,7 +478,8 @@ jquery.ui.draggable: - core/jquery.ui.widget jquery.ui.droppable: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.droppable.js: {} dependencies: @@ -434,103 +489,118 @@ jquery.ui.droppable: - core/jquery.ui.draggable jquery.ui.effects.core: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.core.js: { weight: -9 } jquery.ui.effects.blind: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.blind.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.bounce: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.bounce.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.clip: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.clip.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.drop: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.drop.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.explode: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.explode.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.fade: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.fade.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.fold: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.fold.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.highlight: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.highlight.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.pulsate: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.pulsate.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.scale: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.scale.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.shake: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.shake.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.slide: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.slide.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.effects.transfer: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.effects.transfer.js: {} dependencies: - core/jquery.ui.effects.core jquery.ui.menu: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.menu.js: {} css: @@ -541,19 +611,22 @@ jquery.ui.menu: - core/jquery.ui.widget jquery.ui.mouse: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.mouse.js: {} dependencies: - core/jquery.ui.widget jquery.ui.position: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.position.js: {} jquery.ui.progressbar: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.progressbar.js: {} css: @@ -564,7 +637,8 @@ jquery.ui.progressbar: - core/jquery.ui.widget jquery.ui.resizable: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.resizable.js: {} css: @@ -576,7 +650,8 @@ jquery.ui.resizable: - core/jquery.ui.mouse jquery.ui.selectable: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.selectable.js: {} css: @@ -588,7 +663,8 @@ jquery.ui.selectable: - core/jquery.ui.widget jquery.ui.slider: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.slider.js: {} css: @@ -600,7 +676,8 @@ jquery.ui.slider: - core/jquery.ui.widget jquery.ui.sortable: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.sortable.js: {} dependencies: @@ -609,7 +686,8 @@ jquery.ui.sortable: - core/jquery.ui.widget jquery.ui.spinner: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.spinner.js: {} dependencies: @@ -618,7 +696,8 @@ jquery.ui.spinner: - core/jquery.ui.button jquery.ui.tabs: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.tabs.js: {} css: @@ -629,7 +708,8 @@ jquery.ui.tabs: - core/jquery.ui.widget jquery.ui.tooltip: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.tooltip.js: {} css: @@ -643,13 +723,18 @@ jquery.ui.tooltip: jquery.ui.touch-punch: remote: https://github.com/furf/jquery-ui-touch-punch version: 0.2.2 + license: + name: GNU-GPL-2.0-or-later + remote: https://github.com/furf/jquery-ui-touch-punch + gpl-compatible: true js: assets/vendor/jquery-ui-touch-punch/jquery.ui.touch-punch.js: {} dependencies: - core/jquery.ui jquery.ui.widget: - version: *jquery_ui + version: *jquery_ui_version + license: *jquery_ui_license js: assets/vendor/jquery.ui/ui/jquery.ui.widget.js: { weight: -10 } dependencies: @@ -667,6 +752,10 @@ matchmedia: modernizr: remote: https://github.com/Modernizr/Modernizr + license: + name: MIT + remote: https://github.com/Modernizr/Modernizr/blob/v2.6.2/readme.md + gpl-compatible: true version: v2.6.2 js: assets/vendor/modernizr/modernizr.min.js: { every_page: true, preprocess: 0, scope: header, weight: -21 } @@ -674,6 +763,10 @@ modernizr: normalize: remote: https://github.com/necolas/normalize.css version: 3.0.1 + license: + name: MIT + remote: https://github.com/necolas/normalize.css/blob/master/LICENSE.md + gpl-compatible: true css: base: assets/vendor/normalize-css/normalize.css: { every_page: true, weight: -20 } @@ -691,6 +784,9 @@ picturefill: underscore: remote: https://github.com/jashkenas/underscore version: 1.5.2 + license: + name: MIT + remote: https://github.com/jashkenas/underscore/blob/1.5.2/LICENSE + gpl-compatible: true js: assets/vendor/underscore/underscore.js: { weight: -20 } - diff --git a/core/core.services.yml b/core/core.services.yml index 51a41fa..5f22e4d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -786,7 +786,7 @@ services: arguments: [ '@state' ] asset.js.collection_optimizer: class: Drupal\Core\Asset\JsCollectionOptimizer - arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state' ] + arguments: [ '@asset.js.collection_grouper', '@asset.js.optimizer', '@asset.js.dumper', '@state', '@url_generator' ] asset.js.optimizer: class: Drupal\Core\Asset\JsOptimizer asset.js.collection_grouper: diff --git a/core/includes/common.inc b/core/includes/common.inc index 0735dfa..8cc0cf7 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2702,7 +2702,8 @@ function drupal_attach_tabledrag(&$element, array $options) { */ function drupal_clear_js_cache() { \Drupal::state()->delete('system.javascript_parsed'); - \Drupal::state()->delete('system.js_cache_files'); + \Drupal::state()->delete('system.js_optimized_aggregate_files'); + \Drupal::state()->delete('system.js_unoptimized_aggregate_files'); file_scan_directory('public://js', '/.*/', array('callback' => 'drupal_delete_file_if_stale')); } diff --git a/core/includes/theme.inc b/core/includes/theme.inc index b62ad74..51d4ece 100644 --- a/core/includes/theme.inc +++ b/core/includes/theme.inc @@ -2082,6 +2082,14 @@ function template_preprocess_html(&$variables) { $variables['head'] = new RenderWrapper('drupal_get_html_head'); $variables['styles'] = new RenderWrapper('drupal_get_css'); $variables['scripts'] = new RenderWrapper('drupal_get_js'); + + // 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 4126e57..5d3ba68 100644 --- a/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php +++ b/core/lib/Drupal/Core/Asset/JsCollectionOptimizer.php @@ -6,9 +6,9 @@ namespace Drupal\Core\Asset; +use Drupal\Core\Routing\UrlGeneratorInterface; use Drupal\Core\State\StateInterface; - /** * Optimizes JavaScript assets. */ @@ -43,6 +43,13 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { protected $state; /** + * The URL generator. + * + * @var \Drupal\Core\Routing\UrlGeneratorInterface + */ + protected $urlGenerator; + + /** * Constructs a JsCollectionOptimizer. * * @param \Drupal\Core\Asset\AssetCollectionGrouperInterface @@ -53,12 +60,15 @@ class JsCollectionOptimizer implements AssetCollectionOptimizerInterface { * The dumper for optimized JS assets. * @param \Drupal\Core\State\StateInterface * The state key/value store. + * @param \Drupal\Core\Routing\UrlGeneratorInterface + * The URL generator. */ - public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state) { + public function __construct(AssetCollectionGrouperInterface $grouper, AssetOptimizerInterface $optimizer, AssetDumperInterface $dumper, StateInterface $state, UrlGeneratorInterface $url_generator) { $this->grouper = $grouper; $this->optimizer = $optimizer; $this->dumper = $dumper; $this->state = $state; + $this->urlGenerator = $url_generator; } /** @@ -81,11 +91,13 @@ public function optimize(array $js_assets) { // Now optimize (concatenate, not minify) and dump each asset group, unless // that was already done, in which case it should appear in - // system.js_cache_files. + // system.js_optimized_aggregate_files (but also the unoptimized equivalent + // in system.js_unoptimized_aggregate_files, for JavaScript Web License + // Labels compliance). // 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('system.js_optimized_aggregate_files') ?: 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 +121,8 @@ public function optimize(array $js_assets) { $uri = $map[$key]; } if (empty($uri) || !file_exists($uri)) { - // Concatenate each asset within the group. + $unoptimized_map = $this->state->get('system.js_unoptimized_aggregate_files') ?: array(); + // Concatenate and optimize each asset within the group. $data = ''; foreach ($js_group['items'] as $js_asset) { $data .= $this->optimizer->optimize($js_asset); @@ -123,7 +136,21 @@ 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('system.js_optimized_aggregate_files', $map); + + // For JavaScript License Web Labels compliance, we must also + // create an unoptimized (unminified) variant. These are tracked + // in another map that is kept in sync. + $data = ''; + foreach ($js_group['items'] as $js_asset) { + $url = $this->urlGenerator->generateFromRoute('system.javascript_license_web_labels', array(), array('fragment' => drupal_clean_css_identifier($js_asset['data']))); + $data .= "/** JavaScript asset: " . $js_asset['data'] . '; for license information, see ' . $url . " **/\n\n"; + $data .= file_get_contents($js_asset['data']); + $data .= ";\n"; + } + $unoptimized_uri = $this->dumper->dump($data, 'js'); + $unoptimized_map[$key] = $unoptimized_uri; + $this->state->set('system.js_unoptimized_aggregate_files', $unoptimized_map); } else { // Use the persisted URI for the optimized JS file. diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscovery.php b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php index 2a25ea3..2798bf3 100644 --- a/core/lib/Drupal/Core/Asset/LibraryDiscovery.php +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php @@ -84,7 +84,7 @@ protected function ensureLibraryInformation($extension) { $this->libraries[$extension] = $information; } else { - $this->libraries[$extension] = FALSE; + $this->libraries[$extension] = array(); } $this->setCache($extension, $this->libraries[$extension]); } @@ -174,6 +174,15 @@ protected function buildLibrariesByExtension($extension) { } } + // Assign Drupal's license to libraries that don't have license info. + if (!isset($library['license'])) { + $library['license'] = array( + 'name' => 'GNU-GPL-2.0-or-later', + 'remote' => 'https://drupal.org/licensing/faq', + 'gpl-compatible' => TRUE, + ); + } + foreach (array('js', 'css') as $type) { // Prepare (flatten) the SMACSS-categorized definitions. // @todo After Asset(ic) changes, retain the definitions as-is and @@ -249,6 +258,13 @@ protected function buildLibrariesByExtension($extension) { $options['version'] = $library['version']; } + // Don't aggregate JavaScript assets whose license is incompatible + // with the GPL; Drupal creates GPL-compatible JavaScript aggregates. + // Drupal does this for JavaScript License Web Labels compliance. + if ($type === 'js' && !$library['license']['gpl-compatible']) { + $options['preprocess'] = FALSE; + } + $library[$type][] = $options; } } diff --git a/core/modules/system/lib/Drupal/system/Controller/AssetLicenseInfoController.php b/core/modules/system/lib/Drupal/system/Controller/AssetLicenseInfoController.php new file mode 100644 index 0000000..ad2cedf --- /dev/null +++ b/core/modules/system/lib/Drupal/system/Controller/AssetLicenseInfoController.php @@ -0,0 +1,158 @@ +get('library.discovery') + ); + } + + /** + * Constructs a AssetLicenseInfoController object. + * + * @param \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery + * The asset library discovery service. + */ + public function __construct(LibraryDiscoveryInterface $library_discovery) { + $this->libraryDiscovery = $library_discovery; + } + + /** + * 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']['remote']); + 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 = \Drupal::state()->get('system.js_optimized_aggregate_files'); + if (isset($aggregated_js_assets)) { + $unoptimized_aggregated_js_assets = \Drupal::state()->get('system.js_unoptimized_aggregate_files'); + $license_column = l('GNU-GPL-2.0-or-later', 'https://drupal.org/licensing/faq'); + 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/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php index f6f3da8..7131877 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php @@ -38,6 +38,11 @@ public static function getInfo() { 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(); @@ -460,11 +465,11 @@ function testAggregationOrder() { drupal_render($scripts_html); // Store the expected key for the first item in the cache. - $cache = array_keys(\Drupal::state()->get('system.js_cache_files') ?: array()); + $cache = array_keys(\Drupal::state()->get('system.js_optimized_aggregate_files') ?: array()); $expected_key = $cache[0]; // Reset variables and add a file in a different scope first. - \Drupal::state()->delete('system.js_cache_files'); + \Drupal::state()->delete('system.js_optimized_aggregate_file'); drupal_static_reset('_drupal_add_js'); $attached = array(); $attached['#attached']['library'][] = 'core/drupal'; @@ -485,7 +490,7 @@ function testAggregationOrder() { drupal_render($scripts_html); // Compare the expected key for the first file to the current one. - $cache = array_keys(\Drupal::state()->get('system.js_cache_files') ?: array()); + $cache = array_keys(\Drupal::state()->get('system.js_optimized_aggregate_files') ?: array()); $key = $cache[0]; $this->assertEqual($key, $expected_key, 'JavaScript aggregation is not affected by ordering in different scopes.'); } diff --git a/core/modules/system/system.routing.yml b/core/modules/system/system.routing.yml index 13019c2..dadbf74 100644 --- a/core/modules/system/system.routing.yml +++ b/core/modules/system/system.routing.yml @@ -408,3 +408,11 @@ system.batch_page.json: system.update: path: '/core/update.php' + +system.javascript_license_web_labels: + path: '/system/jslicense' + defaults: + _content: '\Drupal\system\Controller\AssetLicenseInfoController::jslicense' + _title: 'JavaScript license information' + requirements: + _access: 'TRUE' diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php index 230cac6..815e942 100644 --- a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php @@ -173,7 +173,7 @@ public function testGetLibraryWithMissingLibraryFile() { $this->libraryDiscovery->setPaths('module', 'example_module', $path); $this->assertFalse($this->libraryDiscovery->getLibraryByName('example_module', 'example')); - $this->assertFalse($this->libraryDiscovery->getLibrariesByExtension('example_module')); + $this->assertSame(array(), $this->libraryDiscovery->getLibrariesByExtension('example_module')); } /** @@ -376,6 +376,71 @@ public function testLibraryWithDataTypes() { } /** + * Tests a library with various licenses, some GPL-compatible, some not. + * + * @covers ::buildLibrariesByExtension() + */ + public function testLibraryWithLicenses() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('licenses') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'licenses', $path); + + // For libraries without license info, the default license is applied. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'no-license-info'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertTrue(isset($library['license'])); + $default_license = array( + 'name' => 'GNU-GPL-2.0-or-later', + 'remote' => 'https://drupal.org/licensing/faq', + 'gpl-compatible' => TRUE, + ); + $this->assertEquals($library['license'], $default_license, 'Default license is applied.'); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'Library without license: CSS is preprocessable.'); + $this->assertFalse(isset($library['js'][0]['preprocess']), 'Library without license: JS is preprocessable.'); + + // GPL2-licensed libraries are preprocessable. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'gpl2'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'GPL2 library: CSS is preprocessable.'); + $this->assertFalse(isset($library['js'][0]['preprocess']), 'GPL2 library: JS is preprocessable.'); + + // MIT-licensed libraries are preprocessable. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'mit'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'MIT library: CSS is preprocessable.'); + $this->assertFalse(isset($library['js'][0]['preprocess']), 'MIT library: JS is preprocessable.'); + + // Libraries in the Public Domain are preprocessable. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'public-domain'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'Public Domain library: CSS is preprocessable.'); + $this->assertFalse(isset($library['js'][0]['preprocess']), 'Public Domain library: JS is preprocessable.'); + + // Apache-licensed libraries are not preprocessable. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'apache'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'Apache library: CSS is preprocessable.'); + $this->assertFalse($library['js'][0]['preprocess'], 'Apache library: JS is not preprocessable.'); + + // Copyrighted libraries are preprocessable. + $library = $this->libraryDiscovery->getLibraryByName('licenses', 'copyright'); + $this->assertCount(1, $library['css']); + $this->assertCount(1, $library['js']); + $this->assertFalse(isset($library['css'][0]['preprocess']), 'Copyrighted library: CSS is preprocessable.'); + $this->assertFalse($library['js'][0]['preprocess'], 'Copyrighted library: JS is not preprocessable.'); + } + + /** * Tests the internal static cache. * * @covers ::ensureLibraryInformation() diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/licenses.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/licenses.libraries.yml new file mode 100644 index 0000000..3284c96 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/licenses.libraries.yml @@ -0,0 +1,72 @@ +# No license information: should default to GPL 2.0 or later.. +no-license-info: + version: 1.0 + js: + no-license-info.js: {} + css: + base: + no-license-info.css: {} + +# A library with all assets licensed under the GPL-compatible GPL 2 license. +gpl2: + version: 1.0 + license: + name: gpl2 + remote: https://url-to-gpl2-license + gpl-compatible: true + js: + gpl2.js: {} + css: + base: + gpl2.css: {} + +# A library with all assets licensed under the GPL-compatible MIT license. +mit: + version: 1.0 + license: + name: MIT + remote: https://url-to-mit-license + gpl-compatible: true + js: + mit.js: {} + css: + base: + mit.css: {} + +# A library with all assets licensed under the GPL-compatible Public Domain. +public-domain: + version: 1.0 + license: + name: Public Domain + remote: https://url-to-public-domain-license + gpl-compatible: true + js: + public-domain.js: {} + css: + base: + public-domain.css: {} + +# A library with all assets licensed under the GPL-incompatible Apache license. +apache: + version: 1.0 + license: + name: apache + remote: https://url-to-apache-license + gpl-compatible: false + js: + apache.js: {} + css: + base: + apache.css: {} + +# A library with all assets licensed under the GPL-incompatible copyright. +copyright: + version: 1.0 + license: + name: © Some company + gpl-compatible: false + js: + copyright.js: {} + css: + base: + copyright.css: {}