WebUI: migrate away from inline HTML code

`innerHTML` &  `outerHTML` setter will more or less evaluate the value which could be used to
inject malicious code. So replace them with safer alternatives.

PR #21163.
This commit is contained in:
Chocobo1 2024-08-10 12:55:48 +08:00 committed by GitHub
parent 4570c0ef9e
commit 5afeecbf18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 201 additions and 114 deletions

View file

@ -474,15 +474,26 @@ window.addEventListener("DOMContentLoaded", () => {
margin_left = (category_path.length - 1) * 20; margin_left = (category_path.length - 1) * 20;
} }
const html = `<span class="link" href="#" style="margin-left: ${margin_left}px;" onclick="setCategoryFilter(${hash}); return false;">` const span = document.createElement("span");
+ '<img src="images/view-categories.svg"/>' span.classList.add("link");
+ window.qBittorrent.Misc.escapeHtml(display_name) + " (" + count + ")" + "</span>"; span.href = "#";
const el = new Element("li", { span.style.marginLeft = `${margin_left}px`;
id: hash, span.textContent = `${display_name} (${count})`;
html: html span.addEventListener("click", (event) => {
event.preventDefault();
setCategoryFilter(hash);
}); });
window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(el);
return el; const img = document.createElement("img");
img.src = "images/view-categories.svg";
span.prepend(img);
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.categoriesFilterContextMenu.addTarget(listItem);
return listItem;
}; };
const all = torrentsTable.getRowIds().length; const all = torrentsTable.getRowIds().length;
@ -555,15 +566,25 @@ window.addEventListener("DOMContentLoaded", () => {
tagFilterList.getChildren().each(c => c.destroy()); tagFilterList.getChildren().each(c => c.destroy());
const createLink = function(hash, text, count) { const createLink = function(hash, text, count) {
const html = `<span class="link" href="#" onclick="setTagFilter(${hash}); return false;">` const span = document.createElement("span");
+ '<img src="images/tags.svg"/>' span.classList.add("link");
+ window.qBittorrent.Misc.escapeHtml(text) + " (" + count + ")" + "</span>"; span.href = "#";
const el = new Element("li", { span.textContent = `${text} (${count})`;
id: hash, span.addEventListener("click", (event) => {
html: html event.preventDefault();
setTagFilter(hash);
}); });
window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(el);
return el; const img = document.createElement("img");
img.src = "images/tags.svg";
span.prepend(img);
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.tagsFilterContextMenu.addTarget(listItem);
return listItem;
}; };
const torrentsCount = torrentsTable.getRowIds().length; const torrentsCount = torrentsTable.getRowIds().length;
@ -631,15 +652,25 @@ window.addEventListener("DOMContentLoaded", () => {
trackerFilterList.getChildren().each(c => c.destroy()); trackerFilterList.getChildren().each(c => c.destroy());
const createLink = function(hash, text, count) { const createLink = function(hash, text, count) {
const html = '<span class="link" href="#" onclick="setTrackerFilter(' + hash + ');return false;">' const span = document.createElement("span");
+ '<img src="images/trackers.svg"/>' span.classList.add("link");
+ window.qBittorrent.Misc.escapeHtml(text.replace("%1", count)) + "</span>"; span.href = "#";
const el = new Element("li", { span.textContent = text.replace("%1", count);
id: hash, span.addEventListener("click", (event) => {
html: html event.preventDefault();
setTrackerFilter(hash);
}); });
window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(el);
return el; const img = document.createElement("img");
img.src = "images/trackers.svg";
span.prepend(img);
const listItem = document.createElement("li");
listItem.id = hash;
listItem.appendChild(span);
window.qBittorrent.Filters.trackersFilterContextMenu.addTarget(listItem);
return listItem;
}; };
const torrentsCount = torrentsTable.getRowIds().length; const torrentsCount = torrentsTable.getRowIds().length;

View file

@ -428,7 +428,7 @@ window.qBittorrent.ContextMenu ??= (() => {
const contextTagList = $("contextTagList"); const contextTagList = $("contextTagList");
tagList.forEach((tag, tagHash) => { tagList.forEach((tag, tagHash) => {
const checkbox = contextTagList.getElement(`a[href="#Tag/${tagHash}"] input[type="checkbox"]`); const checkbox = contextTagList.getElement(`a[href="#Tag/${tag.name}"] input[type="checkbox"]`);
const count = tagCount.get(tag.name); const count = tagCount.get(tag.name);
const hasCount = (count !== undefined); const hasCount = (count !== undefined);
const isLesser = (count < selectedRows.length); const isLesser = (count < selectedRows.length);
@ -438,7 +438,7 @@ window.qBittorrent.ContextMenu ??= (() => {
const contextCategoryList = document.getElementById("contextCategoryList"); const contextCategoryList = document.getElementById("contextCategoryList");
category_list.forEach((category, categoryHash) => { category_list.forEach((category, categoryHash) => {
const categoryIcon = contextCategoryList.querySelector(`a[href$="(${categoryHash});"] img`); const categoryIcon = contextCategoryList.querySelector(`a[href$="#Category/${category.name}"] img`);
const count = categoryCount.get(category.name); const count = categoryCount.get(category.name);
const isEqual = ((count !== undefined) && (count === selectedRows.length)); const isEqual = ((count !== undefined) && (count === selectedRows.length));
categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual); categoryIcon.classList.toggle("highlightedCategoryIcon", isEqual);
@ -448,12 +448,24 @@ window.qBittorrent.ContextMenu ??= (() => {
updateCategoriesSubMenu: function(categoryList) { updateCategoriesSubMenu: function(categoryList) {
const contextCategoryList = $("contextCategoryList"); const contextCategoryList = $("contextCategoryList");
contextCategoryList.getChildren().each(c => c.destroy()); contextCategoryList.getChildren().each(c => c.destroy());
contextCategoryList.appendChild(new Element("li", {
html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/>QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>' const createMenuItem = (text, imgURL, clickFn) => {
})); const anchor = document.createElement("a");
contextCategoryList.appendChild(new Element("li", { anchor.textContent = text;
html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/>QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>' anchor.addEventListener("click", clickFn);
}));
const img = document.createElement("img");
img.src = imgURL;
img.alt = text;
anchor.prepend(img);
const item = document.createElement("li");
item.appendChild(anchor);
return item;
};
contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN));
contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(0); }));
const sortedCategories = []; const sortedCategories = [];
categoryList.forEach((category, hash) => sortedCategories.push({ categoryList.forEach((category, hash) => sortedCategories.push({
@ -465,14 +477,25 @@ window.qBittorrent.ContextMenu ??= (() => {
let first = true; let first = true;
for (const { categoryName, categoryHash } of sortedCategories) { for (const { categoryName, categoryHash } of sortedCategories) {
const el = new Element("li", { const anchor = document.createElement("a");
html: `<a href="javascript:torrentSetCategoryFN(${categoryHash});"><img src="images/view-categories.svg"/>${window.qBittorrent.Misc.escapeHtml(categoryName)}</a>` anchor.href = `#Category/${categoryName}`;
anchor.textContent = categoryName;
anchor.addEventListener("click", (event) => {
torrentSetCategoryFN(categoryHash);
}); });
const img = document.createElement("img");
img.src = "images/view-categories.svg";
anchor.prepend(img);
const setCategoryItem = document.createElement("li");
setCategoryItem.appendChild(anchor);
if (first) { if (first) {
el.addClass("separator"); setCategoryItem.addClass("separator");
first = false; first = false;
} }
contextCategoryList.appendChild(el);
contextCategoryList.appendChild(setCategoryItem);
} }
}, },
@ -481,18 +504,23 @@ window.qBittorrent.ContextMenu ??= (() => {
while (contextTagList.firstChild !== null) while (contextTagList.firstChild !== null)
contextTagList.removeChild(contextTagList.firstChild); contextTagList.removeChild(contextTagList.firstChild);
contextTagList.appendChild(new Element("li", { const createMenuItem = (text, imgURL, clickFn) => {
html: '<a href="javascript:torrentAddTagsFN();">' const anchor = document.createElement("a");
+ '<img src="images/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>' anchor.textContent = text;
+ " QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]" anchor.addEventListener("click", clickFn);
+ "</a>"
})); const img = document.createElement("img");
contextTagList.appendChild(new Element("li", { img.src = imgURL;
html: '<a href="javascript:torrentRemoveAllTagsFN();">' img.alt = text;
+ '<img src="images/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>' anchor.prepend(img);
+ " QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"
+ "</a>" const item = document.createElement("li");
})); item.appendChild(anchor);
return item;
};
contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN));
contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN));
const sortedTags = []; const sortedTags = [];
tagList.forEach((tag, hash) => sortedTags.push({ tagList.forEach((tag, hash) => sortedTags.push({
@ -503,14 +531,28 @@ window.qBittorrent.ContextMenu ??= (() => {
for (let i = 0; i < sortedTags.length; ++i) { for (let i = 0; i < sortedTags.length; ++i) {
const { tagName, tagHash } = sortedTags[i]; const { tagName, tagHash } = sortedTags[i];
const el = new Element("li", {
html: `<a href="#Tag/${tagHash}" onclick="event.preventDefault(); torrentSetTagsFN(${tagHash}, !event.currentTarget.getElement('input[type=checkbox]').checked);">` const input = document.createElement("input");
+ '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName) input.type = "checkbox";
+ "</a>" input.addEventListener("click", (event) => {
input.checked = !input.checked;
}); });
const anchor = document.createElement("a");
anchor.href = `#Tag/${tagName}`;
anchor.textContent = tagName;
anchor.addEventListener("click", (event) => {
event.preventDefault();
torrentSetTagsFN(tagHash, !input.checked);
});
anchor.prepend(input);
const setTagItem = document.createElement("li");
setTagItem.appendChild(anchor);
if (i === 0) if (i === 0)
el.addClass("separator"); setTagItem.addClass("separator");
contextTagList.appendChild(el);
contextTagList.appendChild(setTagItem);
} }
} }
}); });

View file

@ -333,10 +333,18 @@ window.qBittorrent.DynamicTable ??= (() => {
}); });
const createLi = function(columnName, text) { const createLi = function(columnName, text) {
const html = '<a href="#' + columnName + '" ><img src="images/checked-completed.svg"/>' + window.qBittorrent.Misc.escapeHtml(text) + "</a>"; const anchor = document.createElement("a");
return new Element("li", { anchor.href = `#${columnName}`;
html: html anchor.textContent = text;
});
const img = document.createElement("img");
img.src = "images/checked-completed.svg";
anchor.prepend(img);
const listItem = document.createElement("li");
listItem.appendChild(anchor);
return listItem;
}; };
const actions = {}; const actions = {};
@ -2095,8 +2103,7 @@ window.qBittorrent.DynamicTable ??= (() => {
}, },
id: dirImgId id: dirImgId
}); });
const html = dirImg.outerHTML + span.outerHTML; td.replaceChildren(dirImg, span);
td.innerHTML = html;
} }
} }
else { // is file else { // is file
@ -2108,7 +2115,7 @@ window.qBittorrent.DynamicTable ??= (() => {
"margin-left": ((node.depth + 1) * 20) "margin-left": ((node.depth + 1) * 20)
} }
}); });
td.innerHTML = span.outerHTML; td.replaceChildren(span);
} }
}; };
@ -2122,7 +2129,7 @@ window.qBittorrent.DynamicTable ??= (() => {
text: value, text: value,
id: fileNameRenamedId, id: fileNameRenamedId,
}); });
td.innerHTML = span.outerHTML; td.replaceChildren(span);
}; };
}, },
@ -2428,8 +2435,7 @@ window.qBittorrent.DynamicTable ??= (() => {
}, },
id: dirImgId id: dirImgId
}); });
const html = collapseIcon.outerHTML + dirImg.outerHTML + span.outerHTML; td.replaceChildren(collapseIcon, dirImg, span);
td.innerHTML = html;
} }
} }
else { else {
@ -2441,7 +2447,7 @@ window.qBittorrent.DynamicTable ??= (() => {
"margin-left": ((node.depth + 1) * 20) "margin-left": ((node.depth + 1) * 20)
} }
}); });
td.innerHTML = span.outerHTML; td.replaceChildren(span);
} }
}; };

View file

@ -165,32 +165,31 @@ window.qBittorrent.PropFiles ??= (() => {
return ($("comboPrio" + id) !== null); return ($("comboPrio" + id) !== null);
}; };
const createPriorityOptionElement = function(priority, selected, html) { const createPriorityCombo = (id, fileId, selectedPriority) => {
const elem = new Element("option"); const createOption = (priority, isSelected, text) => {
elem.value = priority.toString(); const option = document.createElement("option");
elem.innerHTML = html; option.value = priority.toString();
if (selected) option.selected = isSelected;
elem.selected = true; option.textContent = text;
return elem; return option;
}; };
const createPriorityCombo = function(id, fileId, selectedPriority) { const select = document.createElement("select");
const select = new Element("select");
select.id = "comboPrio" + id; select.id = "comboPrio" + id;
select.setAttribute("data-id", id); select.setAttribute("data-id", id);
select.setAttribute("data-file-id", fileId); select.setAttribute("data-file-id", fileId);
select.addClass("combo_priority"); select.addClass("combo_priority");
select.addEventListener("change", fileComboboxChanged); select.addEventListener("change", fileComboboxChanged);
createPriorityOptionElement(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select); select.appendChild(createOption(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]"));
createPriorityOptionElement(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select); select.appendChild(createOption(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]"));
createPriorityOptionElement(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select); select.appendChild(createOption(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]"));
createPriorityOptionElement(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select); select.appendChild(createOption(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]"));
// "Mixed" priority is for display only; it shouldn't be selectable // "Mixed" priority is for display only; it shouldn't be selectable
const mixedPriorityOption = createPriorityOptionElement(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]"); const mixedPriorityOption = createOption(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
mixedPriorityOption.disabled = true; mixedPriorityOption.disabled = true;
mixedPriorityOption.injectInside(select); select.appendChild(mixedPriorityOption);
return select; return select;
}; };

View file

@ -67,7 +67,7 @@ window.qBittorrent.PropGeneral ??= (() => {
$("torrent_hash_v1").textContent = ""; $("torrent_hash_v1").textContent = "";
$("torrent_hash_v2").textContent = ""; $("torrent_hash_v2").textContent = "";
$("save_path").textContent = ""; $("save_path").textContent = "";
$("comment").innerHTML = ""; $("comment").textContent = "";
$("private").textContent = ""; $("private").textContent = "";
piecesBar.clear(); piecesBar.clear();
}; };

View file

@ -63,7 +63,7 @@ window.qBittorrent.PropWebseeds ??= (() => {
updateRow: function(tr, row) { updateRow: function(tr, row) {
const tds = tr.getElements("td"); const tds = tr.getElements("td");
for (let i = 0; i < row.length; ++i) for (let i = 0; i < row.length; ++i)
tds[i].innerHTML = row[i]; tds[i].textContent = row[i];
return true; return true;
}, },
@ -78,9 +78,9 @@ window.qBittorrent.PropWebseeds ??= (() => {
const tr = new Element("tr"); const tr = new Element("tr");
this.rows.set(url, tr); this.rows.set(url, tr);
for (let i = 0; i < row.length; ++i) { for (let i = 0; i < row.length; ++i) {
const td = new Element("td"); const td = document.createElement("td");
td.innerHTML = row[i]; td.textContent = row[i];
td.injectInside(tr); tr.appendChild(td);
} }
tr.injectInside(this.table); tr.injectInside(this.table);
}, },

View file

@ -174,16 +174,15 @@ window.qBittorrent.Search ??= (() => {
tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg")); tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));
const liElement = new Element("li", { const listItem = document.createElement("li");
id: newTabId, listItem.id = newTabId;
class: "selected", listItem.classList.add("selected");
html: tabElem.outerHTML, listItem.addEventListener("click", (e) => {
}); setActiveTab(listItem);
liElement.addEventListener("click", (e) => {
setActiveTab(liElement);
$("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]"; $("startSearchButton").textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
}); });
$("searchTabs").appendChild(liElement); listItem.appendChild(tabElem);
$("searchTabs").appendChild(listItem);
// unhide the results elements // unhide the results elements
if (numSearchTabs() >= 1) { if (numSearchTabs() >= 1) {
@ -194,7 +193,7 @@ window.qBittorrent.Search ??= (() => {
} }
// select new tab // select new tab
setActiveTab(liElement); setActiveTab(listItem);
searchResultsTable.clear(); searchResultsTable.clear();
resetFilters(); resetFilters();
@ -577,26 +576,27 @@ window.qBittorrent.Search ??= (() => {
} }
}; };
const getSearchCategories = function() { const getSearchCategories = () => {
const populateCategorySelect = function(categories) { const populateCategorySelect = (categories) => {
const categoryHtml = []; const categoryOptions = [];
categories.each((category) => {
const option = new Element("option"); for (const category of categories) {
const option = document.createElement("option");
option.value = category.id; option.value = category.id;
option.textContent = category.name; option.textContent = category.name;
categoryHtml.push(option.outerHTML); categoryOptions.push(option);
}); };
// first category is "All Categories" // first category is "All Categories"
if (categoryHtml.length > 1) { if (categoryOptions.length > 1) {
// add separator // add separator
const option = new Element("option"); const option = document.createElement("option");
option.disabled = true; option.disabled = true;
option.textContent = "──────────"; option.textContent = "──────────";
categoryHtml.splice(1, 0, option.outerHTML); categoryOptions.splice(1, 0, option);
} }
$("categorySelect").innerHTML = categoryHtml.join(""); $("categorySelect").replaceChildren(...categoryOptions);
}; };
const selectedPlugin = $("pluginsSelect").value; const selectedPlugin = $("pluginsSelect").value;
@ -629,7 +629,16 @@ window.qBittorrent.Search ??= (() => {
url: new URI("api/v2/search/plugins"), url: new URI("api/v2/search/plugins"),
method: "get", method: "get",
noCache: true, noCache: true,
onSuccess: function(response) { onSuccess: (response) => {
const createOption = (text, value, disabled = false) => {
const option = document.createElement("option");
if (value !== undefined)
option.value = value;
option.textContent = text;
option.disabled = disabled;
return option;
};
if (response !== prevSearchPluginsResponse) { if (response !== prevSearchPluginsResponse) {
prevSearchPluginsResponse = response; prevSearchPluginsResponse = response;
searchPlugins.length = 0; searchPlugins.length = 0;
@ -637,9 +646,9 @@ window.qBittorrent.Search ??= (() => {
searchPlugins.push(plugin); searchPlugins.push(plugin);
}); });
const pluginsHtml = []; const pluginOptions = [];
pluginsHtml.push('<option value="enabled">QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]</option>'); pluginOptions.push(createOption("QBT_TR(Only enabled)QBT_TR[CONTEXT=SearchEngineWidget]", "enabled"));
pluginsHtml.push('<option value="all">QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]</option>'); pluginOptions.push(createOption("QBT_TR(All plugins)QBT_TR[CONTEXT=SearchEngineWidget]", "all"));
const searchPluginsEmpty = (searchPlugins.length === 0); const searchPluginsEmpty = (searchPlugins.length === 0);
if (!searchPluginsEmpty) { if (!searchPluginsEmpty) {
@ -656,14 +665,14 @@ window.qBittorrent.Search ??= (() => {
allPlugins.each((plugin) => { allPlugins.each((plugin) => {
if (plugin.enabled === true) if (plugin.enabled === true)
pluginsHtml.push("<option value='" + window.qBittorrent.Misc.escapeHtml(plugin.name) + "'>" + window.qBittorrent.Misc.escapeHtml(plugin.fullName) + "</option>"); pluginOptions.push(createOption(plugin.fullName, plugin.name));
}); });
if (pluginsHtml.length > 2) if (pluginOptions.length > 2)
pluginsHtml.splice(2, 0, "<option disabled>──────────</option>"); pluginOptions.splice(2, 0, createOption("──────────", undefined, true));
} }
$("pluginsSelect").innerHTML = pluginsHtml.join(""); $("pluginsSelect").replaceChildren(...pluginOptions);
$("searchPattern").disabled = searchPluginsEmpty; $("searchPattern").disabled = searchPluginsEmpty;
$("categorySelect").disabled = searchPluginsEmpty; $("categorySelect").disabled = searchPluginsEmpty;