Project

General

Profile

Feature #15766 ยป recent_queries_fwlog.patch

Alexander Snyder, 07/22/2026 09:05 PM

View differences:

usr/local/www/status_logs_filter.php
424 424
//]]>
425 425
</script>
426 426

  
427
<!-- BEGIN: Recent Queries for Advanced Log Filter (RedMine #15766) -->
428
<script type="text/javascript">
429
//<![CDATA[
430
(function() {
431
	var STORAGE_KEY = 'pf_fwlog_recent_queries_v1';
432
	var MAX_ITEMS   = 5;
433

  
434
	// Advanced Log Filter text fields: storage-key -> DOM id
435
	var TEXT_FIELDS = {
436
		src:     'filterlogentries_sourceipaddress',
437
		dst:     'filterlogentries_destinationipaddress',
438
		time:    'filterlogentries_time',
439
		sport:   'filterlogentries_sourceport',
440
		proto:   'filterlogentries_protocol',
441
		qty:     'filterlogentries_qty',
442
		iface:   'filterlogentries_interfaces',
443
		dport:   'filterlogentries_destinationport',
444
		flags:   'filterlogentries_protocolflags',
445
		tracker: 'filterlogentries_tracker'
446
	};
447
	var CHECK_FIELDS = { pass: 'actpass', block: 'actblock' };
448

  
449
	function byId(id) { return document.getElementById(id); }
450

  
451
	function readForm() {
452
		var q = {};
453
		for (var k in TEXT_FIELDS) {
454
			var el = byId(TEXT_FIELDS[k]);
455
			q[k] = el ? String(el.value || '').trim() : '';
456
		}
457
		for (var c in CHECK_FIELDS) {
458
			var ce = byId(CHECK_FIELDS[c]);
459
			q[c] = !!(ce && ce.checked);
460
		}
461
		return q;
462
	}
463

  
464
	function isEmpty(q) {
465
		for (var k in TEXT_FIELDS) { if (q[k]) { return false; } }
466
		return !q.pass && !q.block;
467
	}
468

  
469
	// Only the fields that identify the query (ignore qty) form its identity.
470
	function signature(q) {
471
		var parts = [];
472
		for (var k in TEXT_FIELDS) { if (k !== 'qty') { parts.push(k + '=' + (q[k] || '')); } }
473
		parts.push('pass=' + (q.pass ? 1 : 0));
474
		parts.push('block=' + (q.block ? 1 : 0));
475
		return parts.join('|');
476
	}
477

  
478
	function label(q) {
479
		var bits = [];
480
		if (q.src)     { bits.push('src ' + q.src); }
481
		if (q.dst)     { bits.push('dst ' + q.dst); }
482
		if (q.sport)   { bits.push('sport ' + q.sport); }
483
		if (q.dport)   { bits.push('dport ' + q.dport); }
484
		if (q.proto)   { bits.push('proto ' + q.proto); }
485
		if (q.iface)   { bits.push('if ' + q.iface); }
486
		if (q.flags)   { bits.push('flags ' + q.flags); }
487
		if (q.tracker) { bits.push('tracker ' + q.tracker); }
488
		if (q.time)    { bits.push('time ' + q.time); }
489
		var act = [];
490
		if (q.pass)  { act.push('Pass'); }
491
		if (q.block) { act.push('Block'); }
492
		if (act.length) { bits.push('[' + act.join('+') + ']'); }
493
		return bits.length ? bits.join(', ') : '(empty)';
494
	}
495

  
496
	function load() {
497
		try {
498
			var raw = window.localStorage.getItem(STORAGE_KEY);
499
			var arr = raw ? JSON.parse(raw) : [];
500
			return Array.isArray(arr) ? arr : [];
501
		} catch (e) { return []; }
502
	}
503

  
504
	function save(arr) {
505
		try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); } catch (e) {}
506
	}
507

  
508
	function remember(q) {
509
		if (isEmpty(q)) { return; }
510
		var sig = signature(q);
511
		var arr = load().filter(function(item) { return signature(item) !== sig; });
512
		arr.unshift(q);
513
		if (arr.length > MAX_ITEMS) { arr = arr.slice(0, MAX_ITEMS); }
514
		save(arr);
515
	}
516

  
517
	function applyQuery(q) {
518
		for (var k in TEXT_FIELDS) {
519
			var el = byId(TEXT_FIELDS[k]);
520
			if (el) { el.value = (q[k] != null ? q[k] : ''); }
521
		}
522
		for (var c in CHECK_FIELDS) {
523
			var ce = byId(CHECK_FIELDS[c]);
524
			if (ce) { ce.checked = !!q[c]; }
525
		}
526
		// Populate only: do NOT auto-submit. Bring Apply Filter into view and focus it.
527
		var btn = byId('filterlogentries_submit');
528
		if (btn) {
529
			if (btn.scrollIntoView) { btn.scrollIntoView({block: 'center'}); }
530
			btn.focus();
531
		}
532
	}
533

  
534
	function esc(s) {
535
		return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;')
536
			.replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
537
	}
538

  
539
	function render() {
540
		var host = byId('filter-form');
541
		if (!host) { return; }
542
		var old = byId('pf-recentq'); if (old) { old.parentNode.removeChild(old); }
543

  
544
		var arr = load();
545
		var wrap = document.createElement('div');
546
		wrap.id = 'pf-recentq';
547
		wrap.style.margin = '0 0 10px 0';
548
		wrap.style.padding = '8px 10px';
549
		wrap.style.borderLeft = '3px solid #337ab7';
550

  
551
		var html = '<b>Recent Queries</b>';
552
		if (!arr.length) {
553
			html += ' <span class="text-muted">&ndash; none yet; run a filter to populate this list.</span>';
554
			wrap.innerHTML = html;
555
		} else {
556
			html += ' <a href="#" id="pf-recentq-clear" class="text-muted" style="margin-left:8px;">(clear all)</a>';
557
			html += '<ul class="list-unstyled" style="margin:6px 0 0 0;">';
558
			for (var i = 0; i < arr.length; i++) {
559
				html += '<li style="padding:2px 0;">' +
560
					'<a href="#" class="pf-rq-apply" data-idx="' + i + '" title="Populate the filter with this query">' +
561
					'<i class="fa-solid fa-clock-rotate-left"></i> ' + esc(label(arr[i])) + '</a>' +
562
					' <a href="#" class="pf-rq-del text-muted" data-idx="' + i + '" title="Remove" style="margin-left:6px;">&times;</a>' +
563
					'</li>';
564
			}
565
			html += '</ul>';
566
			wrap.innerHTML = html;
567
		}
568
		host.insertBefore(wrap, host.firstChild);
569

  
570
		var applyLinks = wrap.getElementsByClassName('pf-rq-apply');
571
		for (var a = 0; a < applyLinks.length; a++) {
572
			applyLinks[a].addEventListener('click', function(ev) {
573
				ev.preventDefault();
574
				var idx = parseInt(this.getAttribute('data-idx'), 10);
575
				var list = load();
576
				if (list[idx]) { applyQuery(list[idx]); }
577
			});
578
		}
579
		var delLinks = wrap.getElementsByClassName('pf-rq-del');
580
		for (var d = 0; d < delLinks.length; d++) {
581
			delLinks[d].addEventListener('click', function(ev) {
582
				ev.preventDefault();
583
				var idx = parseInt(this.getAttribute('data-idx'), 10);
584
				var list = load();
585
				list.splice(idx, 1);
586
				save(list);
587
				render();
588
			});
589
		}
590
		var clear = byId('pf-recentq-clear');
591
		if (clear) {
592
			clear.addEventListener('click', function(ev) {
593
				ev.preventDefault();
594
				save([]);
595
				render();
596
			});
597
		}
598
	}
599

  
600
	function init() {
601
		// Only run on the Advanced Log Filter view (fields absent in raw/simple mode).
602
		if (!byId('filterlogentries_sourceipaddress')) { return; }
603
		remember(readForm());
604
		render();
605
	}
606

  
607
	// pfSense runs queued functions once the DOM + jQuery are ready.
608
	if (typeof events !== 'undefined' && events.push) {
609
		events.push(init);
610
	} else if (document.addEventListener) {
611
		document.addEventListener('DOMContentLoaded', init);
612
	}
613
})();
614
//]]>
615
</script>
616
<!-- END: Recent Queries for Advanced Log Filter -->
617

  
427 618
<?php include("foot.inc");
428 619
?>
    (1-1/1)