From a6ba2a8eb88d28a0ff790c6c384dfda91d00c3f3 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Thu, 7 Sep 2023 19:35:17 +0200 Subject: [PATCH 1/3] Implement support for adding multiple domains, clients, groups, and list addresses at once Signed-off-by: DL6ER --- scripts/pi-hole/js/groups-adlists.js | 23 +++++++++--- scripts/pi-hole/js/groups-clients.js | 55 +++++++++++++++++----------- scripts/pi-hole/js/groups-domains.js | 38 +++++++++++-------- scripts/pi-hole/js/groups.js | 21 ++++++++--- 4 files changed, 91 insertions(+), 46 deletions(-) diff --git a/scripts/pi-hole/js/groups-adlists.js b/scripts/pi-hole/js/groups-adlists.js index 84ff709c2..d5363c012 100644 --- a/scripts/pi-hole/js/groups-adlists.js +++ b/scripts/pi-hole/js/groups-adlists.js @@ -499,13 +499,21 @@ function delItems(ids) { function addAdlist(event) { const type = event.data.type; - const address = utils.escapeHtml($("#new_address").val()); const comment = utils.escapeHtml($("#new_comment").val()); + // Check if the user wants to add multiple domains (space or newline separated) + // If so, split the input and store it in an array + var addresses = utils.escapeHtml($("#new_address").val()).split(/[\s,]+/); + // Remove empty elements + addresses = addresses.filter(function (el) { + return el !== ""; + }); + const addressestr = JSON.stringify(addresses); + utils.disableAll(); - utils.showAlert("info", "", "Adding subscribed " + type + "list...", address); + utils.showAlert("info", "", "Adding subscribed " + type + "list(s)...", addressestr); - if (address.length === 0) { + if (addresses.length === 0) { // enable the ui elements again utils.enableAll(); utils.showAlert("warning", "", "Warning", "Please specify " + type + "list address"); @@ -516,10 +524,15 @@ function addAdlist(event) { url: "/api/lists", method: "post", dataType: "json", - data: JSON.stringify({ address: address, comment: comment, type: type }), + data: JSON.stringify({ address: addresses, comment: comment, type: type }), success: function () { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added " + type + "list", address); + utils.showAlert( + "success", + "fas fa-plus", + "Successfully added " + type + "list(s)", + addressestr + ); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-clients.js b/scripts/pi-hole/js/groups-clients.js index 843be7b97..013bab8ba 100644 --- a/scripts/pi-hole/js/groups-clients.js +++ b/scripts/pi-hole/js/groups-clients.js @@ -405,34 +405,47 @@ function delItems(ids) { } function addClient() { - var ip = utils.escapeHtml($("#select").val().trim()); const comment = utils.escapeHtml($("#new_comment").val()); - utils.disableAll(); - utils.showAlert("info", "", "Adding client...", ip); - - if (ip.length === 0) { - utils.enableAll(); - utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address"); - return; - } + // Check if the user wants to add multiple IPs (space or newline separated) + // If so, split the input and store it in an array + var ips = utils.escapeHtml($("#select").val().trim()).split(/[\s,]+/); + // Remove empty elements + ips = ips.filter(function (el) { + return el !== ""; + }); + const ipStr = JSON.stringify(ips); // Validate input, can be: // - IPv4 address (with and without CIDR) // - IPv6 address (with and without CIDR) // - MAC address (in the form AA:BB:CC:DD:EE:FF) // - host name (arbitrary form, we're only checking against some reserved characters) - if (utils.validateIPv4CIDR(ip) || utils.validateIPv6CIDR(ip) || utils.validateMAC(ip)) { - // Convert input to upper case (important for MAC addresses) - ip = ip.toUpperCase(); - } else if (!utils.validateHostname(ip)) { + for (var i = 0; i < ips.length; i++) { + if ( + utils.validateIPv4CIDR(ips[i]) || + utils.validateIPv6CIDR(ips[i]) || + utils.validateMAC(ips[i]) + ) { + // Convert input to upper case (important for MAC addresses) + ips[i] = ips[i].toUpperCase(); + } else if (!utils.validateHostname(ips[i])) { + utils.showAlert( + "warning", + "", + "Warning", + "Input is neither a valid IP or MAC address nor a valid host name!" + ); + return; + } + } + + utils.disableAll(); + utils.showAlert("info", "", "Adding client(s)...", ipStr); + + if (ips.length === 0) { utils.enableAll(); - utils.showAlert( - "warning", - "", - "Warning", - "Input is neither a valid IP or MAC address nor a valid host name!" - ); + utils.showAlert("warning", "", "Warning", "Please specify a client IP or MAC address"); return; } @@ -440,10 +453,10 @@ function addClient() { url: "/api/clients", method: "post", dataType: "json", - data: JSON.stringify({ client: ip, comment: comment }), + data: JSON.stringify({ client: ips, comment: comment }), success: function () { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added client", ip); + utils.showAlert("success", "fas fa-plus", "Successfully added client(s)", ipStr); reloadClientSuggestions(); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-domains.js b/scripts/pi-hole/js/groups-domains.js index 1429a95a1..a4591253b 100644 --- a/scripts/pi-hole/js/groups-domains.js +++ b/scripts/pi-hole/js/groups-domains.js @@ -497,45 +497,53 @@ function addDomain() { commentEl = $("#new_regex_comment"); } - var domain = utils.escapeHtml(domainEl.val()); const comment = utils.escapeHtml(commentEl.val()); + // Check if the user wants to add multiple domains (space or newline separated) + // If so, split the input and store it in an array + var domains = utils.escapeHtml(domainEl.val()).split(/[\s,]+/); + // Remove empty elements + domains = domains.filter(function (el) { + return el !== ""; + }); + const domainStr = JSON.stringify(domains); + utils.disableAll(); - utils.showAlert("info", "", "Adding domain...", domain); + utils.showAlert("info", "", "Adding domain(s)...", domainStr); - if (domain.length < 2) { + if (domains.length === 0) { utils.enableAll(); - utils.showAlert("warning", "", "Warning", "Please specify a domain"); + utils.showAlert("warning", "", "Warning", "Please specify at least one domain"); return; } - // strip "*." if specified by user in wildcard mode - if (kind === "exact" && wildcardChecked && domain.startsWith("*.")) { - domain = domain.substr(2); + for (var i = 0; i < domains.length; i++) { + if (kind === "exact" && wildcardChecked) { + // Transform domain to wildcard if specified by user + domains[i] = "(\\.|^)" + domains[i].replaceAll(".", "\\.") + "$"; + kind = "regex"; + + // strip leading "*." if specified by user in wildcard mode + if (domains[i].startsWith("*.")) domains[i] = domains[i].substr(2); + } } // determine list type const type = action === "add_deny" ? "deny" : "allow"; - // Transform domain to wildcard if specified by user - if (kind === "exact" && wildcardChecked) { - domain = "(\\.|^)" + domain.replaceAll(".", "\\.") + "$"; - kind = "regex"; - } - $.ajax({ url: "/api/domains/" + type + "/" + kind, method: "post", dataType: "json", data: JSON.stringify({ - domain: domain, + domain: domains, comment: comment, type: type, kind: kind, }), success: function () { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added domain", domain); + utils.showAlert("success", "fas fa-plus", "Successfully added domain(s)", domainStr); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups.js b/scripts/pi-hole/js/groups.js index 0beab5baf..eb14b0ebc 100644 --- a/scripts/pi-hole/js/groups.js +++ b/scripts/pi-hole/js/groups.js @@ -277,13 +277,24 @@ function delItems(ids) { } function addGroup() { - const name = utils.escapeHtml($("#new_name").val()); const comment = utils.escapeHtml($("#new_comment").val()); + // Check if the user wants to add multiple groups (space or newline separated) + // If so, split the input and store it in an array + var names = utils + .escapeHtml($("#new_name")) + .val() + .split(/[\s,]+/); + // Remove empty elements + names = names.filter(function (el) { + return el !== ""; + }); + const groupStr = JSON.stringify(names); + utils.disableAll(); - utils.showAlert("info", "", "Adding group...", name); + utils.showAlert("info", "", "Adding group(s)...", groupStr); - if (name.length === 0) { + if (names.length === 0) { // enable the ui elements again utils.enableAll(); utils.showAlert("warning", "", "Warning", "Please specify a group name"); @@ -295,13 +306,13 @@ function addGroup() { method: "post", dataType: "json", data: JSON.stringify({ - name: name, + name: names, comment: comment, enabled: true, }), success: function () { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added group", name); + utils.showAlert("success", "fas fa-plus", "Successfully added group(s)", groupStr); $("#new_name").val(""); $("#new_comment").val(""); table.ajax.reload(); From d516aec5ce6e01dd24c6bc6aeeb8ea87c57d344f Mon Sep 17 00:00:00 2001 From: DL6ER Date: Sun, 8 Oct 2023 10:55:28 +0200 Subject: [PATCH 2/3] Give details about sucessfully added and failed items to domains, groups, lists and clients Signed-off-by: DL6ER --- scripts/pi-hole/js/groups-adlists.js | 9 +--- scripts/pi-hole/js/groups-clients.js | 4 +- scripts/pi-hole/js/groups-domains.js | 4 +- scripts/pi-hole/js/groups.js | 4 +- scripts/pi-hole/js/utils.js | 76 ++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/scripts/pi-hole/js/groups-adlists.js b/scripts/pi-hole/js/groups-adlists.js index d5363c012..f00d387d1 100644 --- a/scripts/pi-hole/js/groups-adlists.js +++ b/scripts/pi-hole/js/groups-adlists.js @@ -525,14 +525,9 @@ function addAdlist(event) { method: "post", dataType: "json", data: JSON.stringify({ address: addresses, comment: comment, type: type }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert( - "success", - "fas fa-plus", - "Successfully added " + type + "list(s)", - addressestr - ); + utils.listsAlert("list", addresses, data); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-clients.js b/scripts/pi-hole/js/groups-clients.js index 013bab8ba..d16df1b3a 100644 --- a/scripts/pi-hole/js/groups-clients.js +++ b/scripts/pi-hole/js/groups-clients.js @@ -454,9 +454,9 @@ function addClient() { method: "post", dataType: "json", data: JSON.stringify({ client: ips, comment: comment }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added client(s)", ipStr); + utils.listsAlert("client", ips, data); reloadClientSuggestions(); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups-domains.js b/scripts/pi-hole/js/groups-domains.js index a4591253b..2185ef061 100644 --- a/scripts/pi-hole/js/groups-domains.js +++ b/scripts/pi-hole/js/groups-domains.js @@ -541,9 +541,9 @@ function addDomain() { type: type, kind: kind, }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added domain(s)", domainStr); + utils.listsAlert("domain", domains, data); table.ajax.reload(null, false); table.rows().deselect(); diff --git a/scripts/pi-hole/js/groups.js b/scripts/pi-hole/js/groups.js index eb14b0ebc..c61b06681 100644 --- a/scripts/pi-hole/js/groups.js +++ b/scripts/pi-hole/js/groups.js @@ -310,9 +310,9 @@ function addGroup() { comment: comment, enabled: true, }), - success: function () { + success: function (data) { utils.enableAll(); - utils.showAlert("success", "fas fa-plus", "Successfully added group(s)", groupStr); + utils.listsAlert("group", names, data); $("#new_name").val(""); $("#new_comment").val(""); table.ajax.reload(); diff --git a/scripts/pi-hole/js/utils.js b/scripts/pi-hole/js/utils.js index cbeb63041..b1823a5d4 100644 --- a/scripts/pi-hole/js/utils.js +++ b/scripts/pi-hole/js/utils.js @@ -536,6 +536,81 @@ function hexDecode(string) { return back; } +function listAlert(type, items, data) { + // Show simple success message if there is no "processed" object in "data" or + // if all items were processed successfully + if (data.processed === undefined || data.processed.success.length === items.length) { + showAlert( + "success", + "fas fa-plus", + "Successfully added " + type + (items.length !== 1 ? "s" : ""), + items.join(", ") + ); + return; + } + + // Show a more detailed message if there is a "processed" object in "data" and + // not all items were processed successfully + let message = ""; + + // Show a list of successful items if there are any + if (data.processed.success.length > 0) { + message += + "Successfully added " + + data.processed.success.length + + " " + + type + + (data.processed.success.length !== 1 ? "s" : "") + + ":"; + + // Loop over data.processed.success and print "item" + for (const item in data.processed.success) { + if (Object.prototype.hasOwnProperty.call(data.processed.success, item)) { + message += "
- " + data.processed.success[item].item + ""; + } + } + } + + // Add a line break if there are both successful and failed items + if (data.processed.success.length > 0 && data.processed.errors.length > 0) { + message += "

"; + } + + // Show a list of failed items if there are any + if (data.processed.errors.length > 0) { + message += + "Failed to add " + + data.processed.errors.length + + " " + + type + + (data.processed.errors.length !== 1 ? "s" : "") + + ":\n"; + + // Loop over data.processed.errors and print "item: error" + for (const item in data.processed.errors) { + if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) { + let error = data.processed.errors[item].error; + if (error === "UNIQUE constraint failed: domainlist.domain, domainlist.type") { + // Replace the error message with a more user-friendly one + error = "Already present"; + } + + message += "
- " + data.processed.errors[item].item + ": " + error; + } + } + } + + // Show the warning message + const total = data.processed.success.length + data.processed.errors.length; + const processed = "(" + total + " " + type + (total !== 1 ? "s" : "") + " processed)"; + showAlert( + "warning", + "fas fa-exclamation-triangle", + "Some " + type + (items.length !== 1 ? "s" : "") + " could not be added " + processed, + message + ); +} + window.utils = (function () { return { escapeHtml: escapeHtml, @@ -569,5 +644,6 @@ window.utils = (function () { parseQueryString: parseQueryString, hexEncode: hexEncode, hexDecode: hexDecode, + listsAlert: listAlert, }; })(); From b46a8a5bb61549a8d602556ce4e4c758e9ff2d71 Mon Sep 17 00:00:00 2001 From: DL6ER Date: Mon, 23 Oct 2023 21:21:29 +0200 Subject: [PATCH 3/3] Address review comment Signed-off-by: DL6ER --- scripts/pi-hole/js/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/pi-hole/js/utils.js b/scripts/pi-hole/js/utils.js index c2486e415..264c7b1a6 100644 --- a/scripts/pi-hole/js/utils.js +++ b/scripts/pi-hole/js/utils.js @@ -591,8 +591,8 @@ function listAlert(type, items, data) { for (const item in data.processed.errors) { if (Object.prototype.hasOwnProperty.call(data.processed.errors, item)) { let error = data.processed.errors[item].error; - if (error === "UNIQUE constraint failed: domainlist.domain, domainlist.type") { - // Replace the error message with a more user-friendly one + // Replace some error messages with a more user-friendly text + if (error.indexOf("UNIQUE constraint failed") > -1) { error = "Already present"; }