diff --git a/messages.php b/messages.php index 9fce17c5..d6c6b15a 100644 --- a/messages.php +++ b/messages.php @@ -23,6 +23,7 @@ ID +   Time Type Message @@ -31,7 +32,7 @@ Data3 Data4 Data5 - Action +   diff --git a/scripts/pi-hole/js/footer.js b/scripts/pi-hole/js/footer.js index 5e386622..b8ac4238 100644 --- a/scripts/pi-hole/js/footer.js +++ b/scripts/pi-hole/js/footer.js @@ -5,6 +5,7 @@ * This file is copyright under the latest version of the EUPL. * Please see LICENSE file for your rights under this license. */ +/* global utils:false */ //The following functions allow us to display time until pi-hole is enabled after disabling. //Works between all pages @@ -101,21 +102,6 @@ function piholeChange(action, duration) { } } -function checkMessages() { - $.getJSON("api_db.php?status", function (data) { - if ("message_count" in data && data.message_count > 0) { - var title = - data.message_count > 1 - ? "There are " + data.message_count + " warnings. Click for further details." - : "There is one warning. Click for further details."; - - $("#pihole-diagnosis").prop("title", title); - $("#pihole-diagnosis-count").text(data.message_count); - $("#pihole-diagnosis").removeClass("hidden"); - } - }); -} - function testCookies() { if (navigator.cookieEnabled) { return true; @@ -235,9 +221,9 @@ $(function () { initCPUtemp(); // Run check immediately after page loading ... - checkMessages(); + utils.checkMessages(); // ... and once again with five seconds delay - setTimeout(checkMessages, 5000); + setTimeout(utils.checkMessages, 5000); }); // Handle Enable/Disable diff --git a/scripts/pi-hole/js/messages.js b/scripts/pi-hole/js/messages.js index f95777f6..0a889467 100644 --- a/scripts/pi-hole/js/messages.js +++ b/scripts/pi-hole/js/messages.js @@ -142,6 +142,7 @@ $(function () { order: [[0, "asc"]], columns: [ { data: "id", visible: false }, + { data: null, visible: true, width: "15px" }, { data: "timestamp", width: "8%", render: renderTimestamp }, { data: "type", width: "8%" }, { data: "message", orderable: false, render: renderMessage }, @@ -150,9 +151,17 @@ $(function () { { data: "blob3", visible: false }, { data: "blob4", visible: false }, { data: "blob5", visible: false }, - { data: null, width: "80px", orderable: false }, + { data: null, width: "22px", orderable: false }, ], columnDefs: [ + { + targets: 1, + orderable: false, + className: "select-checkbox", + render: function () { + return ""; + }, + }, { targets: "_all", render: $.fn.dataTable.render.text(), @@ -160,6 +169,11 @@ $(function () { ], drawCallback: function () { $('button[id^="deleteMessage_"]').on("click", deleteMessage); + + // Hide buttons if all messages were deleted + var hasRows = this.api().rows({ filter: "applied" }).data().length > 0; + $(".datatable-bt").css("visibility", hasRows ? "visible" : "hidden"); + // Remove visible dropdown to prevent orphaning $("body > .bootstrap-select.dropdown").remove(); }, @@ -168,16 +182,63 @@ $(function () { var button = '"; - $("td:eq(3)", row).html(button); + $("td:eq(4)", row).html(button); }, + select: { + style: "multi", + selector: "td:not(:last-child)", + info: false, + }, + buttons: [ + { + text: '', + titleAttr: "Select All", + className: "btn-sm datatable-bt selectAll", + action: function () { + table.rows({ page: "current" }).select(); + }, + }, + { + text: '', + titleAttr: "Select All", + className: "btn-sm datatable-bt selectMore", + action: function () { + table.rows({ page: "current" }).select(); + }, + }, + { + extend: "selectNone", + text: '', + titleAttr: "Deselect All", + className: "btn-sm datatable-bt removeAll", + }, + { + text: '', + titleAttr: "Delete Selected", + className: "btn-sm datatable-bt deleteSelected", + action: function () { + // For each ".selected" row ... + var ids = []; + $("tr.selected").each(function () { + // ... add the row identified by "data-id". + ids.push(parseInt($(this).attr("data-id"), 10)); + }); + // Delete all selected rows at once + delMsg(ids); + }, + }, + ], dom: - "<'row'<'col-sm-12'f>>" + - "<'row'<'col-sm-4'l><'col-sm-8'p>>" + + "<'row'<'col-sm-6'l><'col-sm-6'f>>" + + "<'row'<'col-sm-3'B><'col-sm-9'p>>" + "<'row'<'col-sm-12'<'table-responsive'tr>>>" + - "<'row'<'col-sm-5'i><'col-sm-7'p>>", + "<'row'<'col-sm-3'B><'col-sm-9'p>>" + + "<'row'<'col-sm-12'i>>", lengthMenu: [ [10, 25, 50, 100, -1], [10, 25, 50, 100, "All"], @@ -198,7 +259,7 @@ $(function () { } // Reset visibility of ID and blob columns - var hiddenCols = [0, 4, 5, 6, 7, 8]; + var hiddenCols = [0, 5, 6, 7, 8, 9]; for (var key in hiddenCols) { if (Object.prototype.hasOwnProperty.call(hiddenCols, key)) { data.columns[hiddenCols[key]].visible = false; @@ -209,42 +270,94 @@ $(function () { return data; }, }); + table.on("init select deselect", function () { + changeButtonStates(); + }); }); +// Show only the appropriate buttons +function changeButtonStates() { + var allRows = table.rows({ filter: "applied" }).data().length; + var pageLength = table.page.len(); + var selectedRows = table.rows(".selected").data().length; + + if (selectedRows === 0) { + // Nothing selected + $(".selectAll").removeClass("hidden"); + $(".selectMore").addClass("hidden"); + $(".removeAll").addClass("hidden"); + $(".deleteSelected").addClass("hidden"); + } else if (selectedRows >= pageLength || selectedRows === allRows) { + // Whole page is selected (or all available messages were selected) + $(".selectAll").addClass("hidden"); + $(".selectMore").addClass("hidden"); + $(".removeAll").removeClass("hidden"); + $(".deleteSelected").removeClass("hidden"); + } else { + // Some rows are selected, but not all + $(".selectAll").addClass("hidden"); + $(".selectMore").removeClass("hidden"); + $(".removeAll").addClass("hidden"); + $(".deleteSelected").removeClass("hidden"); + } +} + +// Remove 'bnt-group' class from container, to avoid grouping +$.fn.dataTable.Buttons.defaults.dom.container.className = "dt-buttons"; + function deleteMessage() { - var tr = $(this).closest("tr"); - var id = tr.attr("data-id"); + // Passes the button data-del-id attribute as ID + var ids = [parseInt($(this).attr("data-del-id"), 10)]; + delMsg(ids); +} + +function delMsg(ids) { + // Check input validity + if (!Array.isArray(ids)) return; + + // Exploit prevention: Return early for non-numeric IDs + for (var id in ids) { + if (Object.hasOwnProperty.call(ids, id) && typeof ids[id] !== "number") return; + } utils.disableAll(); - utils.showAlert("info", "", "Deleting message with ID " + parseInt(id, 10), "..."); + var idstring = ids.join(", "); + utils.showAlert("info", "", "Deleting messages: " + idstring, "..."); + $.ajax({ url: "scripts/pi-hole/php/message.php", method: "post", dataType: "json", - data: { action: "delete_message", id: id, token: token }, - success: function (response) { + data: { action: "delete_message", id: JSON.stringify(ids), token: token }, + }) + .done(function (response) { utils.enableAll(); if (response.success) { - utils.showAlert("success", "far fa-trash-alt", "Successfully deleted message # ", id); - table.row(tr).remove().draw(false).ajax.reload(null, false); - } else { utils.showAlert( - "error", - "", - "Error while deleting message with ID " + id, - response.message + "success", + "far fa-trash-alt", + "Successfully deleted messages: " + idstring, + "" ); + for (var id in ids) { + if (Object.hasOwnProperty.call(ids, id)) { + table.row(id).remove().draw(false).ajax.reload(null, false); + } + } + } else { + utils.showAlert("error", "", "Error while deleting message: " + idstring, response.message); } - }, - error: function (jqXHR, exception) { + + // Clear selection after deletion + table.rows().deselect(); + changeButtonStates(); + }) + .done( + utils.checkMessages // Update icon warnings count + ) + .fail(function (jqXHR, exception) { utils.enableAll(); - utils.showAlert( - "error", - "", - "Error while deleting message with ID " + id, - jqXHR.responseText - ); + utils.showAlert("error", "", "Error while deleting message: " + idstring, jqXHR.responseText); console.log(exception); // eslint-disable-line no-console - }, - }); + }); } diff --git a/scripts/pi-hole/js/utils.js b/scripts/pi-hole/js/utils.js index b9e1a573..cf1b6968 100644 --- a/scripts/pi-hole/js/utils.js +++ b/scripts/pi-hole/js/utils.js @@ -360,6 +360,23 @@ function colorBar(percentage, total, cssClass) { return '
' + bar + "
"; } +function checkMessages() { + $.getJSON("api_db.php?status", function (data) { + if ("message_count" in data && data.message_count > 0) { + var title = + data.message_count > 1 + ? "There are " + data.message_count + " warnings. Click for further details." + : "There is one warning. Click for further details."; + + $("#pihole-diagnosis").prop("title", title); + $("#pihole-diagnosis-count").text(data.message_count); + $("#pihole-diagnosis").removeClass("hidden"); + } else { + $("#pihole-diagnosis").addClass("hidden"); + } + }); +} + window.utils = (function () { return { escapeHtml: escapeHtml, @@ -382,5 +399,6 @@ window.utils = (function () { addFromQueryLog: addFromQueryLog, addTD: addTD, colorBar: colorBar, + checkMessages: checkMessages, }; })(); diff --git a/scripts/pi-hole/php/header.php b/scripts/pi-hole/php/header.php index bae8380f..d6cd327e 100644 --- a/scripts/pi-hole/php/header.php +++ b/scripts/pi-hole/php/header.php @@ -204,6 +204,7 @@ + @@ -223,6 +224,8 @@ + + diff --git a/scripts/pi-hole/php/message.php b/scripts/pi-hole/php/message.php index 9a653a0e..c903e2b0 100644 --- a/scripts/pi-hole/php/message.php +++ b/scripts/pi-hole/php/message.php @@ -42,23 +42,23 @@ function JSON_error($message = null) echo json_encode($response); } +// Delete message identified by IDs if ($_POST['action'] == 'delete_message' && isset($_POST['id'])) { -// Delete message identified by ID try { - - $stmt = $db->prepare('DELETE FROM message WHERE id=:id'); - if (!$stmt) { + $ids = json_decode($_POST['id']); + if(!is_array($ids)) + throw new Exception('Invalid payload: id is not an array'); + // Explot prevention: Ensure all entries in the ID array are integers + foreach($ids as $value) { + if (!is_numeric($value)) + throw new Exception('Invalid payload: id contains non-numeric entries'); + } + $stmt = $db->prepare('DELETE FROM message WHERE id IN ('.implode(",",$ids).')'); + if (!$stmt) throw new Exception('While preparing message statement: ' . $db->lastErrorMsg()); - } - if (!$stmt->bindValue(':id', intval($_POST['id']), SQLITE3_INTEGER)) { - throw new Exception('While binding id to message statement: ' . $db->lastErrorMsg()); - } - - if (!$stmt->execute()) { + if (!$stmt->execute()) throw new Exception('While executing message statement: ' . $db->lastErrorMsg()); - } - $reload = true; JSON_success(); diff --git a/scripts/vendor/datatables.buttons.min.js b/scripts/vendor/datatables.buttons.min.js new file mode 100644 index 00000000..9dfe24e5 --- /dev/null +++ b/scripts/vendor/datatables.buttons.min.js @@ -0,0 +1,51 @@ +/*! + Buttons for DataTables 1.7.1 + ©2016-2021 SpryMedia Ltd - datatables.net/license +*/ +(function(e){"function"===typeof define&&define.amd?define(["jquery","datatables.net"],function(y){return e(y,window,document)}):"object"===typeof exports?module.exports=function(y,w){y||(y=window);w&&w.fn.dataTable||(w=require("datatables.net")(y,w).$);return e(w,y,y.document)}:e(jQuery,window,document)})(function(e,y,w,r){function B(a,b,c){e.fn.animate?a.stop().fadeIn(b,c):(a.css("display","block"),c&&c.call(a))}function C(a,b,c){e.fn.animate?a.stop().fadeOut(b,c):(a.css("display","none"),c&&c.call(a))} +function E(a,b){a=new q.Api(a);b=b?b:a.init().buttons||q.defaults.buttons;return(new t(a,b)).container()}var q=e.fn.dataTable,I=0,J=0,x=q.ext.buttons,t=function(a,b){if(!(this instanceof t))return function(c){return(new t(c,a)).container()};"undefined"===typeof b&&(b={});!0===b&&(b={});Array.isArray(b)&&(b={buttons:b});this.c=e.extend(!0,{},t.defaults,b);b.buttons&&(this.c.buttons=b.buttons);this.s={dt:new q.Api(a),buttons:[],listenKeys:"",namespace:"dtb"+I++};this.dom={container:e("<"+this.c.dom.container.tag+ +"/>").addClass(this.c.dom.container.className)};this._constructor()};e.extend(t.prototype,{action:function(a,b){a=this._nodeToButton(a);if(b===r)return a.conf.action;a.conf.action=b;return this},active:function(a,b){var c=this._nodeToButton(a);a=this.c.dom.button.active;c=e(c.node);if(b===r)return c.hasClass(a);c.toggleClass(a,b===r?!0:b);return this},add:function(a,b){var c=this.s.buttons;if("string"===typeof b){b=b.split("-");var d=this.s;c=0;for(var f=b.length-1;c"),k.conf._collection=k.collection,this._expandButton(k.buttons,k.conf.buttons,!0,d)),n.init&&n.init.call(f.button(k.node),f,e(k.node),n),h++)}}},_buildButton:function(a,b){var c=this.c.dom.button, +d=this.c.dom.buttonLiner,f=this.c.dom.collection,h=this.s.dt,m=function(p){return"function"===typeof p?p(h,k,a):p};b&&f.button&&(c=f.button);b&&f.buttonLiner&&(d=f.buttonLiner);if(a.available&&!a.available(h,a))return!1;var g=function(p,l,v,u){u.action.call(l.button(v),p,l,v,u);e(l.table().node()).triggerHandler("buttons-action.dt",[l.button(v),l,v,u])};f=a.tag||c.tag;var n=a.clickBlurs===r?!0:a.clickBlurs,k=e("<"+f+"/>").addClass(c.className).attr("tabindex",this.s.dt.settings()[0].iTabIndex).attr("aria-controls", +this.s.dt.table().node().id).on("click.dtb",function(p){p.preventDefault();!k.hasClass(c.disabled)&&a.action&&g(p,h,k,a);n&&k.trigger("blur")}).on("keyup.dtb",function(p){13===p.keyCode&&!k.hasClass(c.disabled)&&a.action&&g(p,h,k,a)});"a"===f.toLowerCase()&&k.attr("href","#");"button"===f.toLowerCase()&&k.attr("type","button");d.tag?(f=e("<"+d.tag+"/>").html(m(a.text)).addClass(d.className),"a"===d.tag.toLowerCase()&&f.attr("href","#"),k.append(f)):k.html(m(a.text));!1===a.enabled&&k.addClass(c.disabled); +a.className&&k.addClass(a.className);a.titleAttr&&k.attr("title",m(a.titleAttr));a.attr&&k.attr(a.attr);a.namespace||(a.namespace=".dt-button-"+J++);d=(d=this.c.dom.buttonContainer)&&d.tag?e("<"+d.tag+"/>").addClass(d.className).append(k):k;this._addKey(a);this.c.buttonCreated&&(d=this.c.buttonCreated(a,d));return{conf:a,node:k.get(0),inserter:d,buttons:[],inCollection:b,collection:null}},_nodeToButton:function(a,b){b||(b=this.s.buttons);for(var c=0,d=b.length;c").addClass("dt-button-collection").addClass(f.collectionLayout).css("display","none");a=e(a).addClass(f.contentClassName).attr("role","menu").appendTo(c);h.attr("aria-expanded","true");h.parents("body")[0]!==w.body&&(h=w.body.lastChild);f.collectionTitle&&c.prepend('
'+f.collectionTitle+"
");B(c.insertAfter(h),f.fade);d=e(b.table().container());var g=c.css("position");"dt-container"===f.align&&(h=h.parent(),c.css("width",d.width()));if("absolute"=== +g){var n=h.position();g=e(b.node()).position();c.css({top:g.top+h.outerHeight(),left:n.left});n=c.outerHeight();var k=d.offset().top+d.height();k=g.top+h.outerHeight()+n-k;var p=g.top-n,l=d.offset().top;g=g.top-n-5;(k>l-p||f.dropup)&&-gn+l&&(g-=n+l,d-=k+l,l=g>d?l+d:l+g)):(l=g-n,dd?l+d:l+g))):(d=h.offset().top,l=0,l="button-right"===f.align?v-k:p-n);c.css("left",c.position().left+l)}else d=c.height()/2,d>e(y).height()/2&&(d=e(y).height()/2),c.css("marginTop",-1*d);f.background&&t.background(!0,f.backgroundClassName,f.fade,h);e("div.dt-button-background").on("click.dtb-collection",function(){});e("body").on("click.dtb-collection",function(u){var z=e.fn.addBack?"addBack":"andSelf",F=e(u.target).parent()[0]; +(!e(u.target).parents()[z]().filter(a).length&&!e(F).hasClass("dt-buttons")||e(u.target).hasClass("dt-button-background"))&&m()}).on("keyup.dtb-collection",function(u){27===u.keyCode&&m()});f.autoClose&&setTimeout(function(){b.on("buttons-action.b-internal",function(u,z,F,K){K[0]!==h[0]&&m()})},0);e(c).trigger("buttons-popover.dt")}});t.background=function(a,b,c,d){c===r&&(c=400);d||(d=w.body);a?B(e("
").addClass(b).css("display","none").insertAfter(d),c):C(e("div."+b),c,function(){e(this).removeClass(b).remove()})}; +t.instanceSelector=function(a,b){if(a===r||null===a)return e.map(b,function(h){return h.inst});var c=[],d=e.map(b,function(h){return h.name}),f=function(h){if(Array.isArray(h))for(var m=0,g=h.length;m)<[^<]*)*<\/script>/gi,"");a=a.replace(//g,"");if(!b||b.stripHtml)a=a.replace(/<[^>]*>/g,"");if(!b||b.trim)a=a.replace(/^\s+|\s+$/g,"");if(!b||b.stripNewlines)a=a.replace(/\n/g," ");if(!b||b.decodeEntities)G.innerHTML=a,a=G.value;return a};t.defaults={buttons:["copy","excel","csv","pdf","print"],name:"main",tabIndex:0,dom:{container:{tag:"div",className:"dt-buttons"},collection:{tag:"div",className:""},button:{tag:"button",className:"dt-button", +active:"active",disabled:"disabled"},buttonLiner:{tag:"span",className:""}}};t.version="1.7.1";e.extend(x,{collection:{text:function(a){return a.i18n("buttons.collection","Collection")},className:"buttons-collection",init:function(a,b,c){b.attr("aria-expanded",!1)},action:function(a,b,c,d){a.stopPropagation();d._collection.parents("body").length?this.popover(!1,d):this.popover(d._collection,d)},attr:{"aria-haspopup":!0}},copy:function(a,b){if(x.copyHtml5)return"copyHtml5"},csv:function(a,b){if(x.csvHtml5&& +x.csvHtml5.available(a,b))return"csvHtml5"},excel:function(a,b){if(x.excelHtml5&&x.excelHtml5.available(a,b))return"excelHtml5"},pdf:function(a,b){if(x.pdfHtml5&&x.pdfHtml5.available(a,b))return"pdfHtml5"},pageLength:function(a){a=a.settings()[0].aLengthMenu;var b=[],c=[];if(Array.isArray(a[0]))b=a[0],c=a[1];else for(var d=0;d"+a+"":"";B(e('
').html(a).append(e("
")["string"===typeof b?"html":"append"](b)).css("display","none").appendTo("body"));c!==r&&0!== +c&&(A=setTimeout(function(){d.buttons.info(!1)},c));this.on("destroy.btn-info",function(){d.buttons.info(!1)});return this});q.Api.register("buttons.exportData()",function(a){if(this.context.length)return L(new q.Api(this.context[0]),a)});q.Api.register("buttons.exportInfo()",function(a){a||(a={});var b=a;var c="*"===b.filename&&"*"!==b.title&&b.title!==r&&null!==b.title&&""!==b.title?b.title:b.filename;"function"===typeof c&&(c=c());c===r||null===c?c=null:(-1!==c.indexOf("*")&&(c=c.replace("*",e("head > title").text()).trim()), +c=c.replace(/[^a-zA-Z0-9_\u00A1-\uFFFF\.,\-_ !\(\)]/g,""),(b=D(b.extension))||(b=""),c+=b);b=D(a.title);b=null===b?null:-1!==b.indexOf("*")?b.replace("*",e("head > title").text()||"Exported data"):b;return{filename:c,title:b,messageTop:H(this,a.message||a.messageTop,"top"),messageBottom:H(this,a.messageBottom,"bottom")}});var D=function(a){return null===a||a===r?null:"function"===typeof a?a():a},H=function(a,b,c){b=D(b);if(null===b)return null;a=e("caption",a.table().container()).eq(0);return"*"=== +b?a.css("caption-side")!==c?null:a.length?a.text():"":b},G=e("