Ensure all torrent attributes that might contain malicious HTML entities are encoded. By allowing HTML entities to be rendered it enable malicious torrent files to perform XSS attacks. Resolves: https://dev.deluge-torrent.org/ticket/3459
515 lines
16 KiB
JavaScript
515 lines
16 KiB
JavaScript
/**
|
|
* Deluge.TorrentGrid.js
|
|
*
|
|
* Copyright (c) Damien Churchill 2009-2010 <damoxc@gmail.com>
|
|
*
|
|
* This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
|
|
* the additional special exception to link portions of this program with the OpenSSL library.
|
|
* See LICENSE for more details.
|
|
*/
|
|
|
|
(function () {
|
|
/* Renderers for the Torrent Grid */
|
|
function queueRenderer(value) {
|
|
return value == -1 ? '' : value + 1;
|
|
}
|
|
function torrentNameRenderer(value, p, r) {
|
|
return String.format(
|
|
'<div class="torrent-name x-deluge-{0}">{1}</div>',
|
|
r.data['state'].toLowerCase(),
|
|
Ext.util.Format.htmlEncode(value)
|
|
);
|
|
}
|
|
function torrentSpeedRenderer(value) {
|
|
if (!value) return;
|
|
return fspeed(value);
|
|
}
|
|
function torrentLimitRenderer(value) {
|
|
if (value == -1) return '';
|
|
return fspeed(value * 1024.0);
|
|
}
|
|
function torrentProgressRenderer(value, p, r) {
|
|
value = new Number(value);
|
|
var progress = value;
|
|
var text = _(r.data['state']) + ' ' + value.toFixed(2) + '%';
|
|
if (this.style) {
|
|
var style = this.style;
|
|
} else {
|
|
var style = p.style;
|
|
}
|
|
var width = new Number(style.match(/\w+:\s*(\d+)\w+/)[1]);
|
|
return Deluge.progressBar(value, width - 8, text);
|
|
}
|
|
function seedsRenderer(value, p, r) {
|
|
if (r.data['total_seeds'] > -1) {
|
|
return String.format('{0} ({1})', value, r.data['total_seeds']);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
function peersRenderer(value, p, r) {
|
|
if (r.data['total_peers'] > -1) {
|
|
return String.format('{0} ({1})', value, r.data['total_peers']);
|
|
} else {
|
|
return value;
|
|
}
|
|
}
|
|
function availRenderer(value, p, r) {
|
|
return value < 0 ? '∞' : parseFloat(new Number(value).toFixed(3));
|
|
}
|
|
function trackerRenderer(value, p, r) {
|
|
return String.format(
|
|
'<div style="background: url(' +
|
|
deluge.config.base +
|
|
'tracker/{0}) no-repeat; padding-left: 20px;">{0}</div>',
|
|
Ext.util.Format.htmlEncode(value)
|
|
);
|
|
}
|
|
|
|
function etaSorter(eta) {
|
|
if (eta === 0) return Number.MAX_VALUE;
|
|
if (eta <= -1) return Number.MAX_SAFE_INTEGER;
|
|
return eta;
|
|
}
|
|
|
|
function dateOrNever(date) {
|
|
return date > 0.0 ? fdate(date) : _('Never');
|
|
}
|
|
|
|
function timeOrInf(time) {
|
|
if (time === 0) return '';
|
|
if (time <= -1) return '∞';
|
|
return ftime(time);
|
|
}
|
|
|
|
/**
|
|
* Deluge.TorrentGrid Class
|
|
*
|
|
* @author Damien Churchill <damoxc@gmail.com>
|
|
* @version 1.3
|
|
*
|
|
* @class Deluge.TorrentGrid
|
|
* @extends Ext.grid.GridPanel
|
|
* @constructor
|
|
* @param {Object} config Configuration options
|
|
*/
|
|
Deluge.TorrentGrid = Ext.extend(Ext.grid.GridPanel, {
|
|
// object to store contained torrent ids
|
|
torrents: {},
|
|
|
|
columns: [
|
|
{
|
|
id: 'queue',
|
|
header: '#',
|
|
width: 30,
|
|
sortable: true,
|
|
renderer: queueRenderer,
|
|
dataIndex: 'queue',
|
|
},
|
|
{
|
|
id: 'name',
|
|
header: _('Name'),
|
|
width: 150,
|
|
sortable: true,
|
|
renderer: torrentNameRenderer,
|
|
dataIndex: 'name',
|
|
},
|
|
{
|
|
header: _('Size'),
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: fsize,
|
|
dataIndex: 'total_wanted',
|
|
},
|
|
{
|
|
header: _('Progress'),
|
|
width: 150,
|
|
sortable: true,
|
|
renderer: torrentProgressRenderer,
|
|
dataIndex: 'progress',
|
|
},
|
|
{
|
|
header: _('Seeds'),
|
|
hidden: true,
|
|
width: 60,
|
|
sortable: true,
|
|
renderer: seedsRenderer,
|
|
dataIndex: 'num_seeds',
|
|
},
|
|
{
|
|
header: _('Peers'),
|
|
hidden: true,
|
|
width: 60,
|
|
sortable: true,
|
|
renderer: peersRenderer,
|
|
dataIndex: 'num_peers',
|
|
},
|
|
{
|
|
header: _('Down Speed'),
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: torrentSpeedRenderer,
|
|
dataIndex: 'download_payload_rate',
|
|
},
|
|
{
|
|
header: _('Up Speed'),
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: torrentSpeedRenderer,
|
|
dataIndex: 'upload_payload_rate',
|
|
},
|
|
{
|
|
header: _('ETA'),
|
|
width: 60,
|
|
sortable: true,
|
|
renderer: timeOrInf,
|
|
dataIndex: 'eta',
|
|
},
|
|
{
|
|
header: _('Ratio'),
|
|
hidden: true,
|
|
width: 60,
|
|
sortable: true,
|
|
renderer: availRenderer,
|
|
dataIndex: 'ratio',
|
|
},
|
|
{
|
|
header: _('Avail'),
|
|
hidden: true,
|
|
width: 60,
|
|
sortable: true,
|
|
renderer: availRenderer,
|
|
dataIndex: 'distributed_copies',
|
|
},
|
|
{
|
|
header: _('Added'),
|
|
hidden: true,
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: fdate,
|
|
dataIndex: 'time_added',
|
|
},
|
|
{
|
|
header: _('Complete Seen'),
|
|
hidden: true,
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: dateOrNever,
|
|
dataIndex: 'last_seen_complete',
|
|
},
|
|
{
|
|
header: _('Completed'),
|
|
hidden: true,
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: dateOrNever,
|
|
dataIndex: 'completed_time',
|
|
},
|
|
{
|
|
header: _('Tracker'),
|
|
hidden: true,
|
|
width: 120,
|
|
sortable: true,
|
|
renderer: trackerRenderer,
|
|
dataIndex: 'tracker_host',
|
|
},
|
|
{
|
|
header: _('Download Folder'),
|
|
hidden: true,
|
|
width: 120,
|
|
sortable: true,
|
|
renderer: fplain,
|
|
dataIndex: 'download_location',
|
|
},
|
|
{
|
|
header: _('Owner'),
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: fplain,
|
|
dataIndex: 'owner',
|
|
},
|
|
{
|
|
header: _('Public'),
|
|
hidden: true,
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: fplain,
|
|
dataIndex: 'public',
|
|
},
|
|
{
|
|
header: _('Shared'),
|
|
hidden: true,
|
|
width: 80,
|
|
sortable: true,
|
|
renderer: fplain,
|
|
dataIndex: 'shared',
|
|
},
|
|
{
|
|
header: _('Downloaded'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: fsize,
|
|
dataIndex: 'total_done',
|
|
},
|
|
{
|
|
header: _('Uploaded'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: fsize,
|
|
dataIndex: 'total_uploaded',
|
|
},
|
|
{
|
|
header: _('Remaining'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: fsize,
|
|
dataIndex: 'total_remaining',
|
|
},
|
|
{
|
|
header: _('Down Limit'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: torrentLimitRenderer,
|
|
dataIndex: 'max_download_speed',
|
|
},
|
|
{
|
|
header: _('Up Limit'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: torrentLimitRenderer,
|
|
dataIndex: 'max_upload_speed',
|
|
},
|
|
{
|
|
header: _('Seeds:Peers'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: availRenderer,
|
|
dataIndex: 'seeds_peers_ratio',
|
|
},
|
|
{
|
|
header: _('Last Transfer'),
|
|
hidden: true,
|
|
width: 75,
|
|
sortable: true,
|
|
renderer: ftime,
|
|
dataIndex: 'time_since_transfer',
|
|
},
|
|
],
|
|
|
|
meta: {
|
|
root: 'torrents',
|
|
idProperty: 'id',
|
|
fields: [
|
|
{
|
|
name: 'queue',
|
|
sortType: Deluge.data.SortTypes.asQueuePosition,
|
|
},
|
|
{ name: 'name', sortType: Deluge.data.SortTypes.asName },
|
|
{ name: 'total_wanted', type: 'int' },
|
|
{ name: 'state' },
|
|
{ name: 'progress', type: 'float' },
|
|
{ name: 'num_seeds', type: 'int' },
|
|
{ name: 'total_seeds', type: 'int' },
|
|
{ name: 'num_peers', type: 'int' },
|
|
{ name: 'total_peers', type: 'int' },
|
|
{ name: 'download_payload_rate', type: 'int' },
|
|
{ name: 'upload_payload_rate', type: 'int' },
|
|
{ name: 'eta', type: 'int', sortType: etaSorter },
|
|
{ name: 'ratio', type: 'float' },
|
|
{ name: 'distributed_copies', type: 'float' },
|
|
{ name: 'time_added', type: 'int' },
|
|
{ name: 'tracker_host' },
|
|
{ name: 'download_location' },
|
|
{ name: 'total_done', type: 'int' },
|
|
{ name: 'total_uploaded', type: 'int' },
|
|
{ name: 'total_remaining', type: 'int' },
|
|
{ name: 'max_download_speed', type: 'int' },
|
|
{ name: 'max_upload_speed', type: 'int' },
|
|
{ name: 'seeds_peers_ratio', type: 'float' },
|
|
{ name: 'time_since_transfer', type: 'int' },
|
|
],
|
|
},
|
|
|
|
keys: [
|
|
{
|
|
key: 'a',
|
|
ctrl: true,
|
|
stopEvent: true,
|
|
handler: function () {
|
|
deluge.torrents.getSelectionModel().selectAll();
|
|
},
|
|
},
|
|
{
|
|
key: [46],
|
|
stopEvent: true,
|
|
handler: function () {
|
|
ids = deluge.torrents.getSelectedIds();
|
|
deluge.removeWindow.show(ids);
|
|
},
|
|
},
|
|
],
|
|
|
|
constructor: function (config) {
|
|
config = Ext.apply(
|
|
{
|
|
id: 'torrentGrid',
|
|
store: new Ext.data.JsonStore(this.meta),
|
|
columns: this.columns,
|
|
keys: this.keys,
|
|
region: 'center',
|
|
cls: 'deluge-torrents',
|
|
stripeRows: true,
|
|
autoExpandColumn: 'name',
|
|
autoExpandMin: 150,
|
|
deferredRender: false,
|
|
autoScroll: true,
|
|
stateful: true,
|
|
view: new Ext.ux.grid.BufferView({
|
|
rowHeight: 26,
|
|
scrollDelay: false,
|
|
}),
|
|
},
|
|
config
|
|
);
|
|
Deluge.TorrentGrid.superclass.constructor.call(this, config);
|
|
},
|
|
|
|
initComponent: function () {
|
|
Deluge.TorrentGrid.superclass.initComponent.call(this);
|
|
deluge.events.on('torrentsRemoved', this.onTorrentsRemoved, this);
|
|
deluge.events.on('disconnect', this.onDisconnect, this);
|
|
|
|
this.on('rowcontextmenu', function (grid, rowIndex, e) {
|
|
e.stopEvent();
|
|
var selection = grid.getSelectionModel();
|
|
if (!selection.isSelected(rowIndex)) {
|
|
selection.selectRow(rowIndex);
|
|
}
|
|
deluge.menus.torrent.showAt(e.getPoint());
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Returns the record representing the torrent at the specified index.
|
|
*
|
|
* @param index {int} The row index of the torrent you wish to retrieve.
|
|
* @return {Ext.data.Record} The record representing the torrent.
|
|
*/
|
|
getTorrent: function (index) {
|
|
return this.getStore().getAt(index);
|
|
},
|
|
|
|
/**
|
|
* Returns the currently selected record.
|
|
* @ return {Array/Ext.data.Record} The record(s) representing the rows
|
|
*/
|
|
getSelected: function () {
|
|
return this.getSelectionModel().getSelected();
|
|
},
|
|
|
|
/**
|
|
* Returns the currently selected records.
|
|
*/
|
|
getSelections: function () {
|
|
return this.getSelectionModel().getSelections();
|
|
},
|
|
|
|
/**
|
|
* Return the currently selected torrent id.
|
|
* @return {String} The currently selected id.
|
|
*/
|
|
getSelectedId: function () {
|
|
return this.getSelectionModel().getSelected().id;
|
|
},
|
|
|
|
/**
|
|
* Return the currently selected torrent ids.
|
|
* @return {Array} The currently selected ids.
|
|
*/
|
|
getSelectedIds: function () {
|
|
var ids = [];
|
|
Ext.each(this.getSelectionModel().getSelections(), function (r) {
|
|
ids.push(r.id);
|
|
});
|
|
return ids;
|
|
},
|
|
|
|
update: function (torrents, wipe) {
|
|
var store = this.getStore();
|
|
|
|
// Need to perform a complete reload of the torrent grid.
|
|
if (wipe) {
|
|
store.removeAll();
|
|
this.torrents = {};
|
|
}
|
|
|
|
var newTorrents = [];
|
|
|
|
// Update and add any new torrents.
|
|
for (var t in torrents) {
|
|
var torrent = torrents[t];
|
|
|
|
if (this.torrents[t]) {
|
|
var record = store.getById(t);
|
|
record.beginEdit();
|
|
for (var k in torrent) {
|
|
if (record.get(k) != torrent[k]) {
|
|
record.set(k, torrent[k]);
|
|
}
|
|
}
|
|
record.endEdit();
|
|
} else {
|
|
var record = new Deluge.data.Torrent(torrent);
|
|
record.id = t;
|
|
this.torrents[t] = 1;
|
|
newTorrents.push(record);
|
|
}
|
|
}
|
|
store.add(newTorrents);
|
|
|
|
// Remove any torrents that should not be in the store.
|
|
store.each(function (record) {
|
|
if (!torrents[record.id]) {
|
|
store.remove(record);
|
|
delete this.torrents[record.id];
|
|
}
|
|
}, this);
|
|
store.commitChanges();
|
|
|
|
var sortState = store.getSortState();
|
|
if (!sortState) return;
|
|
store.sort(sortState.field, sortState.direction);
|
|
},
|
|
|
|
// private
|
|
onDisconnect: function () {
|
|
this.getStore().removeAll();
|
|
this.torrents = {};
|
|
},
|
|
|
|
// private
|
|
onTorrentsRemoved: function (torrentIds) {
|
|
var selModel = this.getSelectionModel();
|
|
Ext.each(
|
|
torrentIds,
|
|
function (torrentId) {
|
|
var record = this.getStore().getById(torrentId);
|
|
if (selModel.isSelected(record)) {
|
|
selModel.deselectRow(this.getStore().indexOf(record));
|
|
}
|
|
this.getStore().remove(record);
|
|
delete this.torrents[torrentId];
|
|
},
|
|
this
|
|
);
|
|
},
|
|
});
|
|
deluge.torrents = new Deluge.TorrentGrid();
|
|
})();
|