Skip to content

Commit 0cbc59f

Browse files
committed
This PR adds SORTABLE_HTML to dump options for adding sort links to the headers in tables HTML dumps.
1 parent 9ed5652 commit 0cbc59f

File tree

6 files changed

+250
-94
lines changed

6 files changed

+250
-94
lines changed

data/txt/sha256sums.txt

Lines changed: 89 additions & 80 deletions
Large diffs are not rendered by default.

lib/core/dump.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
from lib.core.replication import Replication
4848
from lib.core.settings import DUMP_FILE_BUFFER_SIZE
4949
from lib.core.settings import HTML_DUMP_CSS_STYLE
50+
from lib.core.settings import HTML_DUMP_CSS_SORTABLE_STYLE
51+
from lib.core.settings import HTML_DUMP_SORTABLE_JAVASCRIPT
5052
from lib.core.settings import IS_WIN
5153
from lib.core.settings import METADB_SUFFIX
5254
from lib.core.settings import MIN_BINARY_DISK_DUMP_SIZE
@@ -541,6 +543,9 @@ def dbTableValues(self, tableValues):
541543
dataToDumpFile(dumpFP, "<meta name=\"generator\" content=\"%s\" />\n" % VERSION_STRING)
542544
dataToDumpFile(dumpFP, "<title>%s</title>\n" % ("%s%s" % ("%s." % db if METADB_SUFFIX not in db else "", table)))
543545
dataToDumpFile(dumpFP, HTML_DUMP_CSS_STYLE)
546+
if conf.dumpSortable:
547+
dataToDumpFile(dumpFP, HTML_DUMP_CSS_SORTABLE_STYLE)
548+
dataToDumpFile(dumpFP, HTML_DUMP_SORTABLE_JAVASCRIPT)
544549
dataToDumpFile(dumpFP, "\n</head>\n<body>\n<table>\n<thead>\n<tr>\n")
545550

546551
if count == 1:

lib/core/enums.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ class REGISTRY_OPERATION(object):
230230
class DUMP_FORMAT(object):
231231
CSV = "CSV"
232232
HTML = "HTML"
233+
SORTABLE_HTML = "SORTABLE_HTML"
233234
SQLITE = "SQLITE"
234235

235236
class HTTP_HEADER(object):

lib/core/settings.py

Lines changed: 147 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -921,29 +921,163 @@
921921

922922
# CSS style used in HTML dump format
923923
HTML_DUMP_CSS_STYLE = """<style>
924-
table{
925-
margin:10;
926-
background-color:#FFFFFF;
927-
font-family:verdana;
928-
font-size:12px;
929-
align:center;
924+
table {
925+
margin: 10px;
926+
background: #fff;
927+
font: 12px verdana;
928+
text-align: center;
930929
}
931930
thead{
932931
font-weight:bold;
933932
background-color:#4F81BD;
934-
color:#FFFFFF;
933+
color: #fff;
935934
}
936935
tr:nth-child(even) {
937-
background-color: #D3DFEE
936+
background-color: #D3DFEE;
938937
}
939-
td{
940-
font-size:12px;
938+
</style>"""
939+
940+
HTML_DUMP_CSS_SORTABLE_STYLE = """
941+
<style>
942+
table thead th {
943+
cursor: pointer;
944+
white-space: nowrap;
945+
position: sticky;
946+
top: 0;
947+
z-index: 1;
941948
}
942-
th{
943-
font-size:12px;
949+
950+
table thead th::after,
951+
table thead th::before {
952+
color: transparent;
953+
}
954+
955+
table thead th::after {
956+
margin-left: 3px;
957+
content: "▸";
958+
}
959+
960+
table thead th:hover::after,
961+
table thead th[aria-sort]::after {
962+
color: inherit;
963+
}
964+
965+
table thead th[aria-sort=descending]::after {
966+
content: "▾";
944967
}
945-
</style>"""
946968
969+
table thead th[aria-sort=ascending]::after {
970+
content: "▴";
971+
}
972+
973+
table thead th.indicator-left::before {
974+
margin-right: 3px;
975+
content: "▸";
976+
}
977+
978+
table thead th.indicator-left[aria-sort=descending]::before {
979+
color: inherit;
980+
content: "▾";
981+
}
982+
983+
table thead th.indicator-left[aria-sort=ascending]::before {
984+
color: inherit;
985+
content: "▴";
986+
}
987+
</style>
988+
"""
989+
HTML_DUMP_SORTABLE_JAVASCRIPT = """<script>
990+
window.addEventListener('DOMContentLoaded', () => {
991+
document.addEventListener('click', event => {
992+
try {
993+
const isAltSort = event.shiftKey || event.altKey;
994+
995+
// Find the clicked table header
996+
const findParentElement = (element, nodeName) =>
997+
element.nodeName === nodeName ? element : findParentElement(element.parentNode, nodeName);
998+
999+
const headerCell = findParentElement(event.target, 'TH');
1000+
const headerRow = headerCell.parentNode;
1001+
const thead = headerRow.parentNode;
1002+
const table = thead.parentNode;
1003+
1004+
if (thead.nodeName !== 'THEAD') return;
1005+
1006+
// Reset sort indicators on other headers
1007+
Array.from(headerRow.cells).forEach(cell => {
1008+
if (cell !== headerCell) cell.removeAttribute('aria-sort');
1009+
});
1010+
1011+
// Toggle sort direction
1012+
const currentSort = headerCell.getAttribute('aria-sort');
1013+
const isAscending = table.classList.contains('asc') && currentSort !== 'ascending';
1014+
const sortDirection = (currentSort === 'descending' || isAscending) ? 'ascending' : 'descending';
1015+
headerCell.setAttribute('aria-sort', sortDirection);
1016+
1017+
// Debounce sort operation
1018+
if (table.dataset.timer) clearTimeout(Number(table.dataset.timer));
1019+
1020+
table.dataset.timer = setTimeout(() => {
1021+
sortTable(table, isAltSort);
1022+
}, 1).toString();
1023+
} catch (error) {
1024+
console.error('Sorting error:', error);
1025+
}
1026+
});
1027+
});
1028+
1029+
function sortTable(table, useAltSort) {
1030+
table.dispatchEvent(new CustomEvent('sort-start', { bubbles: true }));
1031+
1032+
const sortHeader = table.tHead.querySelector('th[aria-sort]');
1033+
const headerRow = table.tHead.children[0];
1034+
const isAscending = sortHeader.getAttribute('aria-sort') === 'ascending';
1035+
const shouldPushEmpty = table.classList.contains('n-last');
1036+
const sortColumnIndex = Number(sortHeader.dataset.sortCol ?? sortHeader.cellIndex);
1037+
1038+
const getCellValue = cell => {
1039+
if (useAltSort) return cell.dataset.sortAlt;
1040+
return cell.dataset.sort ?? cell.textContent;
1041+
};
1042+
1043+
const compareRows = (row1, row2) => {
1044+
const value1 = getCellValue(row1.cells[sortColumnIndex]);
1045+
const value2 = getCellValue(row2.cells[sortColumnIndex]);
1046+
1047+
// Handle empty values
1048+
if (shouldPushEmpty) {
1049+
if (value1 === '' && value2 !== '') return -1;
1050+
if (value2 === '' && value1 !== '') return 1;
1051+
}
1052+
1053+
// Compare numerically if possible, otherwise use string comparison
1054+
const numericDiff = Number(value1) - Number(value2);
1055+
const comparison = isNaN(numericDiff) ?
1056+
value1.localeCompare(value2, undefined, { numeric: true }) :
1057+
numericDiff;
1058+
1059+
// Handle tiebreaker
1060+
if (comparison === 0 && headerRow.cells[sortColumnIndex]?.dataset.sortTbr) {
1061+
const tiebreakIndex = Number(headerRow.cells[sortColumnIndex].dataset.sortTbr);
1062+
return compareRows(row1, row2, tiebreakIndex);
1063+
}
1064+
1065+
return isAscending ? -comparison : comparison;
1066+
};
1067+
1068+
// Sort each tbody
1069+
Array.from(table.tBodies).forEach(tbody => {
1070+
const rows = Array.from(tbody.rows);
1071+
const sortedRows = rows.sort(compareRows);
1072+
1073+
const newTbody = tbody.cloneNode();
1074+
newTbody.append(...sortedRows);
1075+
tbody.replaceWith(newTbody);
1076+
});
1077+
1078+
table.dispatchEvent(new CustomEvent('sort-end', { bubbles: true }));
1079+
}
1080+
</script>"""
9471081
# Leaving (dirty) possibility to change values from here (e.g. `export SQLMAP__MAX_NUMBER_OF_THREADS=20`)
9481082
for key, value in os.environ.items():
9491083
if key.upper().startswith("%s_" % SQLMAP_ENVIRONMENT_PREFIX):

sqlmap.conf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -758,9 +758,10 @@ csvDel = ,
758758
dumpFile =
759759

760760
# Format of dumped data
761-
# Valid: CSV, HTML or SQLITE
761+
# Valid: CSV, HTML, SORTABLE_HTML or SQLITE
762762
dumpFormat = CSV
763763

764+
dumpSortable = False
764765
# Force character encoding used for data retrieval.
765766
encoding =
766767

sqlmap.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ def main():
158158
if checkPipedInput():
159159
conf.batch = True
160160

161+
if conf.get("dumpFormat") == "SORTABLE_HTML":
162+
conf.dumpFormat = "HTML"
163+
conf.dumpSortable = True
164+
else:
165+
conf.dumpSortable = False
166+
161167
if conf.get("api"):
162168
# heavy imports
163169
from lib.utils.api import StdDbOut

0 commit comments

Comments
 (0)