66
77namespace PrivacyMonitor
88{
9+ /// <summary>Per-tab state: UI refs, request/fingerprint data, and protection state. UI refs are set by MainWindow after creation.
10+ /// BlockedRequests and other lists are not thread-safe; RegisterBlocked and DrainPending must be called on the UI thread.
11+ /// Consider constructor injection for required UI refs when refactoring the creation flow.</summary>
912 public class BrowserTab
1013 {
1114 public string Id { get ; } = Guid . NewGuid ( ) . ToString ( "N" ) [ ..8 ] ;
15+ /// <summary>Set by MainWindow after creation. Not null once tab is fully initialized.</summary>
1216 public WebView2 WebView { get ; set ; } = null ! ;
17+ /// <summary>Set by MainWindow after BuildTabHeader(tab).</summary>
1318 public Border TabHeader { get ; set ; } = null ! ;
1419 public TextBlock TitleBlock { get ; set ; } = null ! ;
1520 public TextBlock InitialBlock { get ; set ; } = null ! ;
@@ -20,11 +25,13 @@ public class BrowserTab
2025 public string Title { get ; set ; } = "New Tab" ;
2126 public string Url { get ; set ; } = "" ;
2227 public string CurrentHost { get ; set ; } = "" ;
23- public DateTime ScanStart { get ; set ; } = DateTime . Now ;
28+ /// <summary>When the current page scan started (UTC). Use UTC for consistency with LastActivityUtc.</summary>
29+ public DateTime ScanStart { get ; set ; } = DateTime . UtcNow ;
2430 /// <summary>Last time this tab had user or network activity (UTC). Used for idle sleep.</summary>
2531 public DateTime LastActivityUtc { get ; set ; } = DateTime . UtcNow ;
2632 /// <summary>Last measured approximate memory usage for this tab (bytes, from JS APIs).</summary>
2733 public long ? LastMemoryBytes { get ; set ; }
34+ /// <summary>Next ID for new requests. Incremented at ingestion (e.g. Interlocked.Increment in the request handler).</summary>
2835 public int NextRequestId ;
2936 public bool IsReady { get ; set ; }
3037 public bool IsLoading { get ; set ; }
@@ -38,17 +45,25 @@ public class BrowserTab
3845
3946 // ── Thread-safe request ingestion ──
4047 public ConcurrentQueue < RequestEntry > PendingRequests { get ; } = new ( ) ;
48+ /// <summary>Max requests kept per tab. Consider making configurable via settings.</summary>
4149 public const int MaxRequests = 5000 ;
50+ /// <summary>Max blocked-request records kept. Consider making configurable.</summary>
4251 public const int MaxBlockedRequests = 2000 ;
4352
44- // ── Processed data (UI-thread only) ──
45- public List < RequestEntry > Requests { get ; } = new ( ) ;
53+ // ── Processed data (UI-thread only; not thread-safe) ──
54+ /// <summary>LinkedList for O(1) eviction when over MaxRequests; use Last/Previous to scan recent-first.</summary>
55+ public LinkedList < RequestEntry > Requests { get ; } = new ( ) ;
4656 public List < FingerprintFinding > Fingerprints { get ; } = new ( ) ;
4757 public List < CookieItem > Cookies { get ; } = new ( ) ;
4858 public List < StorageItem > Storage { get ; } = new ( ) ;
4959 public List < WebRtcLeak > WebRtcLeaks { get ; } = new ( ) ;
5060 public List < SecurityHeaderResult > SecurityHeaders { get ; set ; } = new ( ) ;
5161
62+ /// <summary>Raised after one or more requests are drained (UI thread). Optional for UI refresh. Subscribers doing heavy work may want to debounce.</summary>
63+ public event Action ? RequestProcessed ;
64+ /// <summary>Raised when a request is recorded as blocked. Must only be called from the UI thread (BlockedRequests is not thread-safe).</summary>
65+ public event Action < BlockedRequestInfo > ? BlockedRequestAdded ;
66+
5267 // ── Detection context (for cross-request correlation) ──
5368 public HashSet < string > SeenTrackerDomains { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
5469 public HashSet < string > SeenTrackerCompanies { get ; } = new ( StringComparer . OrdinalIgnoreCase ) ;
@@ -58,6 +73,8 @@ public class BrowserTab
5873 // ── Forensic state ──
5974 public Dictionary < string , HashSet < string > > IdentifierToDomains { get ; } = new ( ) ;
6075 public List < DataFlowEdge > DataFlowEdges { get ; } = new ( ) ;
76+ /// <summary>O(1) lookup for AddOrUpdateEdge; cleared with DataFlowEdges on ResetDetection.</summary>
77+ public Dictionary < string , DataFlowEdge > DataFlowEdgeLookup { get ; } = new ( ) ;
6178 public List < ForensicEvent > ForensicTimeline { get ; } = new ( ) ;
6279
6380 // ── Protection state ──
@@ -69,49 +86,107 @@ public class BrowserTab
6986 public string ? FingerprintDetectScriptId { get ; set ; }
7087 public string ? BehavioralMonitorScriptId { get ; set ; }
7188
72- /// <summary>Drain pending queue into Requests list . Call from UI thread.</summary>
89+ /// <summary>Drain pending queue into Requests. Call from UI thread only. Eviction is O(1) via LinkedList .</summary>
7390 public int DrainPending ( )
7491 {
7592 int drained = 0 ;
7693 while ( PendingRequests . TryDequeue ( out var entry ) )
7794 {
78- Requests . Add ( entry ) ;
95+ Requests . AddLast ( entry ) ;
7996 drained ++ ;
8097
81- // Track domain counts
82- DomainRequestCounts [ entry . Host ] = DomainRequestCounts . GetValueOrDefault ( entry . Host ) + 1 ;
98+ var host = entry . Host ?? "" ;
99+ DomainRequestCounts [ host ] = DomainRequestCounts . GetValueOrDefault ( host ) + 1 ;
83100
84- // Track seen trackers
85- if ( ! string . IsNullOrEmpty ( entry . TrackerLabel ) )
101+ if ( ! string . IsNullOrEmpty ( entry . TrackerLabel ) && host . Length > 0 )
86102 {
87- SeenTrackerDomains . Add ( entry . Host ) ;
103+ SeenTrackerDomains . Add ( host ) ;
88104 if ( ! string . IsNullOrEmpty ( entry . TrackerCompany ) )
89105 SeenTrackerCompanies . Add ( entry . TrackerCompany ) ;
90106 }
91107
92- // Forensic correlation (runs on UI thread, lightweight per-request)
93- ForensicEngine . ExtractAndRegisterIdentifiers ( entry , IdentifierToDomains , ForensicTimeline ) ;
94- ForensicEngine . AnalyzeDataFlow ( entry , CurrentHost , DataFlowEdges , ForensicTimeline ) ;
108+ try
109+ {
110+ ForensicEngine . ExtractAndRegisterIdentifiers ( entry , IdentifierToDomains , ForensicTimeline ) ;
111+ ForensicEngine . AnalyzeDataFlow ( entry , CurrentHost , DataFlowEdges , ForensicTimeline , DataFlowEdgeLookup ) ;
112+ }
113+ catch
114+ {
115+ // ForensicEngine failed; avoid breaking drain. Log in host app if needed.
116+ }
95117
96- // Evict oldest if over limit
97118 if ( Requests . Count > MaxRequests )
98- Requests . RemoveAt ( 0 ) ;
119+ Requests . RemoveFirst ( ) ;
99120 }
121+ if ( drained > 0 )
122+ RequestProcessed ? . Invoke ( ) ;
100123 return drained ;
101124 }
102125
103- /// <summary>Reset all detection state (on host change).</summary>
126+ /// <summary>Reset all detection state (on host change). Preserves list references for bindings (e.g. SecurityHeaders.Clear()). </summary>
104127 public void ResetDetection ( )
105128 {
106- Requests . Clear ( ) ; Fingerprints . Clear ( ) ; Cookies . Clear ( ) ;
107- Storage . Clear ( ) ; WebRtcLeaks . Clear ( ) ; SecurityHeaders = new ( ) ;
108- NextRequestId = 0 ; ScanStart = DateTime . Now ; ConsentDetected = false ;
109- SeenTrackerDomains . Clear ( ) ; SeenTrackerCompanies . Clear ( ) ;
110- DomainRequestCounts . Clear ( ) ; SeenUrls . Clear ( ) ;
111- IdentifierToDomains . Clear ( ) ; DataFlowEdges . Clear ( ) ; ForensicTimeline . Clear ( ) ;
112- BlockedCount = 0 ; BlockedRequests . Clear ( ) ; AntiFingerprintInjected = false ;
113- // Drain any pending
129+ Requests . Clear ( ) ;
130+ Fingerprints . Clear ( ) ;
131+ Cookies . Clear ( ) ;
132+ Storage . Clear ( ) ;
133+ WebRtcLeaks . Clear ( ) ;
134+ SecurityHeaders . Clear ( ) ;
135+ NextRequestId = 0 ;
136+ ScanStart = DateTime . UtcNow ;
137+ ConsentDetected = false ;
138+ SeenTrackerDomains . Clear ( ) ;
139+ SeenTrackerCompanies . Clear ( ) ;
140+ DomainRequestCounts . Clear ( ) ;
141+ SeenUrls . Clear ( ) ;
142+ IdentifierToDomains . Clear ( ) ;
143+ DataFlowEdges . Clear ( ) ;
144+ DataFlowEdgeLookup . Clear ( ) ;
145+ ForensicTimeline . Clear ( ) ;
146+ BlockedCount = 0 ;
147+ BlockedRequests . Clear ( ) ;
148+ AntiFingerprintInjected = false ;
114149 while ( PendingRequests . TryDequeue ( out _ ) ) { }
115150 }
151+
152+ /// <summary>Record user or network activity (updates LastActivityUtc for idle sleep).</summary>
153+ public void NoteActivity ( )
154+ {
155+ LastActivityUtc = DateTime . UtcNow ;
156+ }
157+
158+ /// <summary>Mark this tab as crashed (e.g. after ProcessFailed).</summary>
159+ public void MarkCrashed ( )
160+ {
161+ IsCrashed = true ;
162+ }
163+
164+ /// <summary>Mark this tab as sleeping (suspended) or awake.</summary>
165+ public void MarkSleeping ( bool sleeping = true )
166+ {
167+ IsSleeping = sleeping ;
168+ }
169+
170+ /// <summary>Record a blocked request for the forensic trail and badge. Must be called on the UI thread (BlockedRequests is not thread-safe).</summary>
171+ public void RegisterBlocked ( DateTime time , string host , string url , string reason , string category , double confidence , string trackerLabel , string resourceType , string method )
172+ {
173+ BlockedCount ++ ;
174+ var info = new BlockedRequestInfo
175+ {
176+ Time = time ,
177+ Host = host ?? "" ,
178+ Url = url ?? "" ,
179+ Reason = reason ?? "" ,
180+ Category = category ?? "" ,
181+ Confidence = confidence ,
182+ TrackerLabel = trackerLabel ?? "" ,
183+ ResourceType = resourceType ?? "" ,
184+ Method = method ?? ""
185+ } ;
186+ BlockedRequests . Add ( info ) ;
187+ if ( BlockedRequests . Count > MaxBlockedRequests )
188+ BlockedRequests . RemoveAt ( 0 ) ;
189+ BlockedRequestAdded ? . Invoke ( info ) ;
190+ }
116191 }
117192}
0 commit comments