Skip to content

Commit 0037d10

Browse files
committed
Adding ability to select SORTABLE_HTML as dump format to have sortable tables in HTML dumps.
1 parent cc245a0 commit 0037d10

File tree

6 files changed

+164
-16
lines changed

6 files changed

+164
-16
lines changed

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
@@ -229,6 +229,7 @@ class REGISTRY_OPERATION(object):
229229
class DUMP_FORMAT(object):
230230
CSV = "CSV"
231231
HTML = "HTML"
232+
SORTABLE_HTML = "SORTABLE_HTML"
232233
SQLITE = "SQLITE"
233234

234235
class HTTP_HEADER(object):

lib/core/settings.py

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

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

lib/parse/cmdline.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ def cmdLineParser(argv=None):
674674
help="Store dumped data to a custom file")
675675

676676
general.add_argument("--dump-format", dest="dumpFormat",
677-
help="Format of dumped data (CSV (default), HTML or SQLITE)")
677+
help="Format of dumped data (CSV (default), HTML, SORTABLE_HTML or SQLITE)")
678678

679679
general.add_argument("--encoding", dest="encoding",
680680
help="Character encoding used for data retrieval (e.g. GBK)")

sqlmap.conf

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -754,8 +754,10 @@ csvDel = ,
754754
dumpFile =
755755

756756
# Format of dumped data
757-
# Valid: CSV, HTML or SQLITE
758-
dumpFormat = CSV
757+
# Valid: CSV, HTML, SORTABLE_HTML or SQLITE
758+
dumpFormat = SORTABLE_HTML
759+
760+
dumpSortable = False
759761

760762
# Force character encoding used for data retrieval.
761763
encoding =

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)