Implement Content Security Policy for the File Manager

The policy includes default-src:none and explicitly lists what is permitted. 

The only seemingly lax part is "style-src 'unsafe-inline'" which is required because the HTML flattener always inlines all CSS.

BUG=chromium-os:23500
TEST=Photos/videos/music should open normally (both from local folders and from Google Docs).


Review URL: http://codereview.chromium.org/10068007

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@132558 0039d316-1c4b-4281-b951-d872f2087c98
parent e3ba9b98
...@@ -1502,6 +1502,10 @@ bool FileDialogStringsFunction::RunImpl() { ...@@ -1502,6 +1502,10 @@ bool FileDialogStringsFunction::RunImpl() {
if (gdata::util::IsGDataAvailable(profile_)) if (gdata::util::IsGDataAvailable(profile_))
dict->SetString("ENABLE_GDATA", "1"); dict->SetString("ENABLE_GDATA", "1");
#if defined(USE_ASH)
dict->SetString("ASH", "1");
#endif
return true; return true;
} }
......
...@@ -25,10 +25,13 @@ ...@@ -25,10 +25,13 @@
<include name="IDR_BOOKMARK_MANAGER_BOOKMARK_CSS_JS" file="bookmark_manager/css/bmm.css.js" type="BINDATA" /> <include name="IDR_BOOKMARK_MANAGER_BOOKMARK_CSS_JS" file="bookmark_manager/css/bmm.css.js" type="BINDATA" />
<if expr="pp_ifdef('file_manager_extension')"> <if expr="pp_ifdef('file_manager_extension')">
<include name="IDR_FILE_MANAGER_MAIN" file="file_manager/main.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_FILE_MANAGER_MAIN" file="file_manager/main.html" flattenhtml="true" allowexternalscript="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_MEDIAPLAYER" file="file_manager/mediaplayer.html" flattenhtml="true" type="BINDATA" /> <include name="IDR_FILE_MANAGER_MAIN_JS" file="file_manager/js/main_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_MEDIAPLAYER" file="file_manager/mediaplayer.html" allowexternalscript="true" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_MEDIAPLAYER_JS" file="file_manager/js/media/mediaplayer_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_IMAGE_EDITOR" file="file_manager/gallery.html" allowexternalscript="true" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_IMAGE_EDITOR_JS" file="file_manager/js/image_editor/gallery_scripts.js" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_UTIL" file="file_manager/js/util.js" type="BINDATA" /> <include name="IDR_FILE_MANAGER_UTIL" file="file_manager/js/util.js" type="BINDATA" />
<include name="IDR_FILE_MANAGER_IMAGE_EDITOR" file="file_manager/gallery.html" flattenhtml="true" type="BINDATA" />
<include name="IDR_FILE_MANAGER_METADATA_DISPATCHER" file="file_manager/js/metadata/metadata_dispatcher.js" type="BINDATA" /> <include name="IDR_FILE_MANAGER_METADATA_DISPATCHER" file="file_manager/js/metadata/metadata_dispatcher.js" type="BINDATA" />
<include name="IDR_FILE_MANAGER_METADATA_READER" file="file_manager/js/metadata/byte_reader.js" type="BINDATA" /> <include name="IDR_FILE_MANAGER_METADATA_READER" file="file_manager/js/metadata/byte_reader.js" type="BINDATA" />
<include name="IDR_FILE_MANAGER_METADATA_PARSER" file="file_manager/js/metadata/metadata_parser.js" type="BINDATA" /> <include name="IDR_FILE_MANAGER_METADATA_PARSER" file="file_manager/js/metadata/metadata_parser.js" type="BINDATA" />
......
...@@ -518,10 +518,10 @@ div.thumbnail-view { ...@@ -518,10 +518,10 @@ div.thumbnail-view {
button.settings { button.settings {
display: none; display: none;
/* Temporary solution not to uglify settings button */
height: 30px;
margin-left: 8px; margin-left: 8px;
width: 57px; width: 57px;
/* Temporary solution not to uglify settings button */
height:30px;
} }
[gdata] button.settings { [gdata] button.settings {
...@@ -981,43 +981,6 @@ li.table-row { ...@@ -981,43 +981,6 @@ li.table-row {
background-image: url(../images/filetype_gtable.png); background-image: url(../images/filetype_gtable.png);
} }
/* The filename text in the preview pane. */
.preview-filename {
-webkit-margin-start: 8px;
color: #666;
font-weight: bold;
margin-top: 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* The preview image. */
.preview-img {
margin-top: 10px;
max-height: 300px;
max-width: 190px;
}
.preview-img.thumbnail {
-webkit-background-clip: content-box;
/* Checkboard background to distinguish images with alpha channels, from
* ../images/preview-background.png
*/
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOTQAADiYBwNzaZQAAAAd0SU1FB9sDExUSAaQ/5TMAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAQ0lEQVRYw+3VsQkAMQwDQOfJRt7r9/FQ3ilDuAiBUy84UKFVVX8M0t2TenxxOQAAAAAAAAB7+ueZaQIAAAAAAIC3AQceAAfi8EmRSgAAAABJRU5ErkJggg==);
border: 1px #666 solid;
padding: 1px;
}
.preview-img[src=''] {
visibility: hidden;
}
/* Decoration when multiple images are selected. */
.preview-img.multiple-selected {
-webkit-box-shadow: 5px 5px 0 #aaa;
}
.metadata-item { .metadata-item {
-webkit-box-flex: 1; -webkit-box-flex: 1;
-webkit-box-orient: horizontal; -webkit-box-orient: horizontal;
...@@ -1254,3 +1217,12 @@ div.unmounted-panel > div { ...@@ -1254,3 +1217,12 @@ div.unmounted-panel > div {
.unmounted-panel:not([retry]) > .gdata.learn-more { .unmounted-panel:not([retry]) > .gdata.learn-more {
display: none; display: none;
} }
body[ash] .dialog-title,
body[type='full-page'] [invisibleif='full-page'],
body:not([type='full-page']) [visibleif='full-page'],
body:not([type='saveas-file']) [visibleif='saveas-file'],
body:not([type='saveas-file']):not([type='full-page'])
[visibleif='saveas-file full-page'] {
display: none;
}
<!-- <!--
-- Copyright (c) 2011 The Chromium Authors. All rights reserved. -- Copyright (c) 2012 The Chromium Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be -- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file. -- found in the LICENSE file.
--> -->
<!--
Load this page into a tab or an iframe, then set the gallery up like this:
iframe.contentWindow.Gallery.open(parentDirEntry, entries, closeCallback);
-->
<html> <html>
<head> <head>
<link rel="stylesheet" type="text/css" href="css/gallery.css"/> <link rel="stylesheet" type="text/css" href="css/gallery.css"/>
<link rel="stylesheet" type="text/css" href="css/media_controls.css"/> <link rel="stylesheet" type="text/css" href="css/media_controls.css"/>
<script type="text/javascript" src="js/image_editor/image_util.js"></script> <script src="js/image_editor/gallery_scripts.js"></script>
<script type="text/javascript" src="js/image_editor/viewport.js"></script>
<script type="text/javascript" src="js/image_editor/image_buffer.js"></script>
<script type="text/javascript" src="js/image_editor/image_view.js"></script>
<script type="text/javascript" src="js/image_editor/commands.js"></script>
<script type="text/javascript" src="js/image_editor/image_editor.js"></script>
<script type="text/javascript" src="js/image_editor/image_transform.js"></script>
<script type="text/javascript" src="js/image_editor/image_adjust.js"></script>
<script type="text/javascript" src="js/image_editor/filter.js"></script>
<script type="text/javascript" src="js/image_editor/gallery.js"></script>
<script type="text/javascript" src="js/image_editor/image_encoder.js"></script>
<script type="text/javascript" src="js/image_editor/exif_encoder.js"></script>
<script type="text/javascript" src="js/media/media_controls.js"></script>
<if expr="0">
<!-- This file has not been flattened, load individual scripts.
Keep the list in sync with gallery_scripts.js. -->
<script src="js/image_editor/image_util.js"></script>
<script src="js/image_editor/viewport.js"></script>
<script src="js/image_editor/image_buffer.js"></script>
<script src="js/image_editor/image_view.js"></script>
<script src="js/image_editor/commands.js"></script>
<script src="js/image_editor/image_editor.js"></script>
<script src="js/image_editor/image_transform.js"></script>
<script src="js/image_editor/image_adjust.js"></script>
<script src="js/image_editor/filter.js"></script>
<script src="js/image_editor/gallery.js"></script>
<script src="js/image_editor/image_encoder.js"></script>
<script src="js/image_editor/exif_encoder.js"></script>
<script src="js/media/media_controls.js"></script>
</if>
</head> </head>
<body> <body>
<div class="gallery"></div> <div class="gallery"></div>
......
...@@ -649,7 +649,6 @@ FileManager.prototype = { ...@@ -649,7 +649,6 @@ FileManager.prototype = {
this.previewThumbnails_ = this.previewThumbnails_ =
this.dialogDom_.querySelector('.preview-thumbnails'); this.dialogDom_.querySelector('.preview-thumbnails');
this.previewPanel_ = this.dialogDom_.querySelector('.preview-panel'); this.previewPanel_ = this.dialogDom_.querySelector('.preview-panel');
this.previewFilename_ = this.dialogDom_.querySelector('.preview-filename');
this.previewSummary_ = this.dialogDom_.querySelector('.preview-summary'); this.previewSummary_ = this.dialogDom_.querySelector('.preview-summary');
this.filenameInput_ = this.dialogDom_.querySelector('.filename-input'); this.filenameInput_ = this.dialogDom_.querySelector('.filename-input');
this.taskItems_ = this.dialogDom_.querySelector('.tasks'); this.taskItems_ = this.dialogDom_.querySelector('.tasks');
...@@ -696,6 +695,11 @@ FileManager.prototype = { ...@@ -696,6 +695,11 @@ FileManager.prototype = {
this.okButton_.addEventListener('click', this.onOk_.bind(this)); this.okButton_.addEventListener('click', this.onOk_.bind(this));
this.cancelButton_.addEventListener('click', this.onCancel_.bind(this)); this.cancelButton_.addEventListener('click', this.onCancel_.bind(this));
this.deleteButton_.addEventListener('click',
this.onDeleteButtonClick_.bind(this));
this.deleteButton_.addEventListener('keypress',
this.onDeleteButtonKeyPress_.bind(this));
this.dialogDom_.querySelector('div.open-sidebar').addEventListener( this.dialogDom_.querySelector('div.open-sidebar').addEventListener(
'click', this.onToggleSidebar_.bind(this)); 'click', this.onToggleSidebar_.bind(this));
this.dialogDom_.querySelector('div.open-sidebar').addEventListener( this.dialogDom_.querySelector('div.open-sidebar').addEventListener(
...@@ -734,12 +738,8 @@ FileManager.prototype = { ...@@ -734,12 +738,8 @@ FileManager.prototype = {
this.dialogDom_.ownerDocument.defaultView.addEventListener( this.dialogDom_.ownerDocument.defaultView.addEventListener(
'resize', this.onResize_.bind(this)); 'resize', this.onResize_.bind(this));
var ary = this.dialogDom_.querySelectorAll('[visibleif]'); if (str('ASH') == '1')
for (var i = 0; i < ary.length; i++) { this.dialogDom_.setAttribute('ash', 'true');
var expr = ary[i].getAttribute('visibleif');
if (!eval(expr))
ary[i].style.display = 'none';
}
this.filePopup_ = null; this.filePopup_ = null;
...@@ -1731,8 +1731,10 @@ FileManager.prototype = { ...@@ -1731,8 +1731,10 @@ FileManager.prototype = {
this.okButton_.textContent = okLabel; this.okButton_.textContent = okLabel;
dialogTitle = this.params_.title || defaultTitle; var dialogTitle = this.params_.title || defaultTitle;
this.dialogDom_.querySelector('.dialog-title').textContent = dialogTitle; this.dialogDom_.querySelector('.dialog-title').textContent = dialogTitle;
this.dialogDom_.setAttribute('type', this.dialogType_);
}; };
FileManager.prototype.renderCheckbox_ = function() { FileManager.prototype.renderCheckbox_ = function() {
...@@ -3159,6 +3161,23 @@ FileManager.prototype = { ...@@ -3159,6 +3161,23 @@ FileManager.prototype = {
this.directoryModel_.deleteEntries(entries, opt_callback); this.directoryModel_.deleteEntries(entries, opt_callback);
}; };
FileManager.prototype.onDeleteButtonClick_ = function(event) {
this.deleteEntries(this.selection.entries);
event.preventDefault();
event.stopPropagation();
},
FileManager.prototype.onDeleteButtonKeyPress_ = function(event) {
switch (util.getKeyModifiers(event) + event.keyCode) {
case '13': // Enter
case '32': // Space
this.deleteEntries(this.selection.entries);
event.preventDefault();
event.stopPropagation();
break;
}
},
FileManager.prototype.blinkSelection = function() { FileManager.prototype.blinkSelection = function() {
if (!this.selection || this.selection.totalCount == 0) if (!this.selection || this.selection.totalCount == 0)
return; return;
......
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The include directives are put into Javascript-style comments to prevent
// parsing errors in non-flattened mode. The flattener still sees them.
// Note that this makes the flattener to comment out the first line of the
// included file but that's all right since any javascript file should start
// with a copyright comment anyway.
//<include src="image_util.js"/>
//<include src="viewport.js"/>
//<include src="image_buffer.js"/>
//<include src="image_view.js"/>
//<include src="commands.js"/>
//<include src="image_editor.js"/>
//<include src="image_transform.js"/>
//<include src="image_adjust.js"/>
//<include src="filter.js"/>
//<include src="gallery.js"/>
//<include src="image_encoder.js"/>
//<include src="exif_encoder.js"/>
//<include src="../media/media_controls.js"/>
// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
...@@ -13,7 +13,7 @@ var fileManager; ...@@ -13,7 +13,7 @@ var fileManager;
* Called by main.html after the dom has been parsed. * Called by main.html after the dom has been parsed.
*/ */
function init() { function init() {
FileManager.initStrings(function () { FileManager.initStrings(function() {
metrics.startInterval('Load.Construct'); metrics.startInterval('Load.Construct');
fileManager = new FileManager(document.body); fileManager = new FileManager(document.body);
metrics.recordInterval('Load.Construct'); metrics.recordInterval('Load.Construct');
...@@ -23,3 +23,7 @@ function init() { ...@@ -23,3 +23,7 @@ function init() {
chrome.test.sendMessage('ready'); chrome.test.sendMessage('ready');
}); });
} }
document.addEventListener('DOMContentLoaded', init);
metrics.recordInterval('Load.Script'); // Must be the last line.
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The include directives are put into Javascript-style comments to prevent
// parsing errors in non-flattened mode. The flattener still sees them.
// Note that this makes the flattener to comment out the first line of the
// included file but that's all right since any javascript file should start
// with a copyright comment anyway.
// //metrics.js initiates load performance tracking
// //so we want to parse it as early as possible.
//<include src="metrics.js"/>
//
//<include src="../../shared/js/local_strings.js"/>
//<include src="../../shared/js/util.js"/>
//<include src="../../shared/js/i18n_template.js"/>
//
//<include src="../../shared/js/cr.js"/>
//<include src="../../shared/js/event_tracker.js"/>
//<include src="../../shared/js/cr/ui.js"/>
//<include src="../../shared/js/cr/event_target.js"/>
//<include src="../../shared/js/cr/ui/touch_handler.js"/>
//<include src="../../shared/js/cr/ui/array_data_model.js"/>
//<include src="../../shared/js/cr/ui/dialogs.js"/>
//<include src="../../shared/js/cr/ui/list_item.js"/>
//<include src="../../shared/js/cr/ui/list_selection_model.js"/>
//<include src="../../shared/js/cr/ui/list_single_selection_model.js"/>
//<include src="../../shared/js/cr/ui/list_selection_controller.js"/>
//<include src="../../shared/js/cr/ui/list.js"/>
//
//<include src="../../shared/js/cr/ui/splitter.js"/>
//<include src="../../shared/js/cr/ui/table/table_splitter.js"/>
//
//<include src="../../shared/js/cr/ui/table/table_column.js"/>
//<include src="../../shared/js/cr/ui/table/table_column_model.js"/>
//<include src="../../shared/js/cr/ui/table/table_header.js"/>
//<include src="../../shared/js/cr/ui/table/table_list.js"/>
//<include src="../../shared/js/cr/ui/table.js"/>
//
//<include src="../../shared/js/cr/ui/grid.js"/>
//
//<include src="../../shared/js/cr/ui/command.js"/>
//<include src="../../shared/js/cr/ui/position_util.js"/>
//<include src="../../shared/js/cr/ui/menu_item.js"/>
//<include src="../../shared/js/cr/ui/menu.js"/>
//<include src="../../shared/js/cr/ui/menu_button.js"/>
//<include src="../../shared/js/cr/ui/context_menu_handler.js"/>
//
//<include src="combobutton.js"/>
//
//<include src="util.js"/>
//<include src="directory_model.js"/>
//<include src="file_copy_manager.js"/>
//<include src="file_manager.js"/>
//<include src="file_manager_pyauto.js"/>
//<include src="file_type.js"/>
//<include src="file_transfer_controller.js"/>
//<include src="metadata/metadata_provider.js"/>
// // For accurate load performance tracking place main.js should be
// // the last include to include.
//<include src="main.js"/>
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// The include directives are put into Javascript-style comments to prevent
// parsing errors in non-flattened mode. The flattener still sees them.
// Note that this makes the flattener to comment out the first line of the
// included file but that's all right since any javascript file should start
// with a copyright comment anyway.
//<include src="audio_player.js"/>
//<include src="media_controls.js"/>
//<include src="../metadata/metadata_provider.js"/>
...@@ -8,10 +8,30 @@ ...@@ -8,10 +8,30 @@
* To be included as a first script in main.html * To be included as a first script in main.html
*/ */
(function() {
// Switch to the 'test harness' mode when loading from a file or http url.
// Do this as early as possible because the metrics code depends on
// chrome private APIs.
if (document.location.protocol == 'file:' ||
document.location.protocol == 'http:') {
console.log('created mock script');
document.write('<script src="js/mock_chrome.js"><\57script>');
}
})();
var metrics = {}; var metrics = {};
/**
* A map from interval name to interval start timestamp.
*/
metrics.intervals = {}; metrics.intervals = {};
/**
* Start the named time interval.
* Should be followed by a call to recordInterval with the same name.
*
* @param {string} name Unique interval name.
*/
metrics.startInterval = function(name) { metrics.startInterval = function(name) {
metrics.intervals[name] = Date.now(); metrics.intervals[name] = Date.now();
}; };
...@@ -19,12 +39,25 @@ metrics.startInterval = function(name) { ...@@ -19,12 +39,25 @@ metrics.startInterval = function(name) {
metrics.startInterval('Load.Total'); metrics.startInterval('Load.Total');
metrics.startInterval('Load.Script'); metrics.startInterval('Load.Script');
/**
* Convert a short metric name to the full format.
*
* @param {string} name Short metric name.
* @return {string} Full metric name.
* @private
*/
metrics.convertName_ = function(name) { metrics.convertName_ = function(name) {
return 'FileBrowser.' + name; return 'FileBrowser.' + name;
}; };
/**
* Create a decorator function that calls a chrome.metricsPrivate function
* with the same name and correct parameters.
*
* @param {string} name Method name.
*/
metrics.decorate = function(name) { metrics.decorate = function(name) {
this[name] = function() { metrics[name] = function() {
var args = Array.apply(null, arguments); var args = Array.apply(null, arguments);
args[0] = metrics.convertName_(args[0]); args[0] = metrics.convertName_(args[0]);
chrome.metricsPrivate[name].apply(chrome.metricsPrivate, args); chrome.metricsPrivate[name].apply(chrome.metricsPrivate, args);
...@@ -39,6 +72,13 @@ metrics.decorate('recordSmallCount'); ...@@ -39,6 +72,13 @@ metrics.decorate('recordSmallCount');
metrics.decorate('recordTime'); metrics.decorate('recordTime');
metrics.decorate('recordUserAction'); metrics.decorate('recordUserAction');
/**
* Complete the time interval recording.
*
* Should be preceded by a call to startInterval with the same name. *
*
* @param {string} name Unique interval name.
*/
metrics.recordInterval = function(name) { metrics.recordInterval = function(name) {
if (name in metrics.intervals) { if (name in metrics.intervals) {
metrics.recordTime(name, Date.now() - metrics.intervals[name]); metrics.recordTime(name, Date.now() - metrics.intervals[name]);
...@@ -47,6 +87,14 @@ metrics.recordInterval = function(name) { ...@@ -47,6 +87,14 @@ metrics.recordInterval = function(name) {
} }
}; };
/**
* Record an enum value.
*
* @param {string} name Metric name.
* @param {Object} value Enum value.
* @param {Array.<Object>|number} validValues Array of valid values
* or a boundary number value.
*/
metrics.recordEnum = function(name, value, validValues) { metrics.recordEnum = function(name, value, validValues) {
var boundaryValue; var boundaryValue;
var index; var index;
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
"clipboardRead", "clipboardRead",
"https://*.googleusercontent.com/" "https://*.googleusercontent.com/"
], ],
"content_security_policy": "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' about:; img-src 'self' data: filesystem: https://*.googleusercontent.com; media-src filesystem: https://*.googleusercontent.com;",
"app": { "app": {
"launch": { "launch": {
"local_path": "main.html" "local_path": "main.html"
......
<!--
-- Copyright (c) 2012 The Chromium Authors. All rights reserved.
-- Use of this source code is governed by a BSD-style license that can be
-- found in the LICENSE file.
-->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd"> "http://www.w3.org/TR/html4/loose.dtd">
<html> <html>
<head> <head>
<!-- We have to set some default title, or chrome will use the page name.
-- As soon as the i18n'd strings are loaded we replace it with the correct
-- string. Until then, use an invisible non-whitespace character.
-->
<title>&#xFEFF;</title>
<link rel="icon" type="image/png" href="images/media/audio_player.png"/> <link rel="icon" type="image/png" href="images/media/audio_player.png"/>
<link rel="stylesheet" type="text/css" href="css/media_controls.css"/> <link rel="stylesheet" type="text/css" href="css/media_controls.css"/>
<link rel="stylesheet" type="text/css" href="css/audio_player.css"/> <link rel="stylesheet" type="text/css" href="css/audio_player.css"/>
<script src="js/media/audio_player.js"></script> <script src="js/media/mediaplayer_scripts.js"></script>
<script src="js/media/media_controls.js"></script>
<script src="js/metadata/metadata_provider.js"></script> <if expr="0">
<!-- This file has not been flattened, load individual scripts.
Keep the list in sync with mediaplayer_scripts.js. -->
<script src="js/media/audio_player.js"></script>
<script src="js/media/media_controls.js"></script>
<script src="js/metadata/metadata_provider.js"></script>
</if>
</head> </head>
<body> <body>
<div class="audio-player"></div> <div class="audio-player"></div>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment