deluge/deluge/ui/web/js/deluge-all/TorrentGrid.js
Calum Lind a5503c0c60
[WebUI] Fix encoding HTML entities for torrent attributes
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
2022-02-14 18:43:20 +00:00

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 ? '&infin;' : 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 '&infin;';
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();
})();