-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpopup.js
More file actions
1499 lines (1274 loc) · 55.4 KB
/
Copy pathpopup.js
File metadata and controls
1499 lines (1274 loc) · 55.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* SimpleCookie, a minimalist yet efficient cookie manager for Firefox */
/* Made with ❤ by micka from Paris */
// ==================== VARIABLES ====================
// Set to hold the tracking list
let trackingSites = new Set();
// Arrays to store cookie and tab objects
let cookies = [];
let tabs = [];
// Array to keep track of cookies that have been deleted for potential undo
let tempDeletedCookies = [];
// Variable to manage the timeout for the undo action
let undoTimeout;
// Global variable to store favorites
let favorites = [];
// Array to store sniper domains
let sniperDomains = [];
// ==================== STORAGE MANAGEMENT ====================
/**
* Loads favorites from browser storage
* @returns {Promise<Array>} Array of favorite domains
*/
async function loadFavorites() {
try {
const data = await browser.storage.local.get('favorites');
favorites = data.favorites || [];
return favorites;
} catch (error) {
console.error('Error loading favorites:', error);
favorites = [];
return [];
}
}
/**
* Saves favorites to browser storage
* @param {Array} favList - Array of favorite domains to save
* @returns {Promise<void>}
*/
async function saveFavorites(favList) {
try {
await browser.storage.local.set({ favorites: favList });
favorites = favList;
} catch (error) {
console.error('Error saving favorites:', error);
}
}
/**
* Loads sniper domains from browser storage
* @returns {Promise<Array>} Array of sniper domains
*/
async function loadSniperDomains() {
try {
const data = await browser.storage.local.get('sniperDomains');
sniperDomains = data.sniperDomains ?
data.sniperDomains.map(d => d.trim().toLowerCase()).filter(d => d) : [];
return sniperDomains;
} catch (error) {
console.error('Error loading sniper domains:', error);
sniperDomains = [];
return [];
}
}
/**
* Saves sniper domains to browser storage
* @param {Array} domains - Array of sniper domains
* @returns {Promise<void>}
*/
async function saveSniperDomains(domains) {
try {
await browser.storage.local.set({ sniperDomains: domains });
sniperDomains = domains;
} catch (error) {
console.error('Error saving sniper domains:', error);
}
}
// ==================== DATA FETCHING ====================
/**
* Fetches the list of tracking websites from the local database
* Loads the data only once (when the set is empty)
*/
async function fetchTrackerDB() {
if (trackingSites.size > 0) return;
try {
const response = await fetch(browser.runtime.getURL('resources/trackerdb.txt'));
const text = await response.text();
trackingSites = new Set(text.split('\n').map(domain => domain.trim()).filter(Boolean));
} catch (error) {
console.error('Failed to load tracker database:', error);
}
}
/**
* Fetches all cookies from all containers and avoids duplicates
* Retrieves both normal and partitioned cookies
* @returns {Promise<Array>} Array of unique cookie objects
*/
async function fetchAllCookies() {
try {
// Fetch all containers
const containers = await browser.contextualIdentities.query({});
const cookiePromises = [];
// Get all store IDs (containers + default)
const storeIds = [...containers.map(container => container.cookieStoreId), ""];
// For each store ID, fetch both normal and partitioned cookies
storeIds.forEach(storeId => {
// Normal cookies
cookiePromises.push(browser.cookies.getAll({ storeId }));
// Partitioned cookies
cookiePromises.push(browser.cookies.getAll({ storeId, partitionKey: {} }));
});
// Wait for all promises to resolve and flatten the result
const allCookies = (await Promise.all(cookiePromises)).flat();
// More complete uniqueness check using all relevant properties
const uniqueMap = new Map();
allCookies.forEach(cookie => {
// Create a comprehensive unique key for each cookie
const key = `${cookie.name}-${cookie.domain}-${cookie.path}-${cookie.storeId}-${cookie.partitionKey ? JSON.stringify(cookie.partitionKey) : 'null'}-${cookie.value}-${cookie.expirationDate || 'session'}-${cookie.secure}-${cookie.httpOnly}-${cookie.sameSite}`;
if (!uniqueMap.has(key)) {
uniqueMap.set(key, cookie);
}
});
return Array.from(uniqueMap.values());
} catch (error) {
console.error('Error fetching cookies:', error);
return [];
}
}
/**
* Fetches all cookies and open tabs
* Updates the cookie counter in the UI
*/
async function fetchCookiesAndTabs() {
// Fetch cookies and store them in the cookies array
cookies = await fetchAllCookies();
// Update the cookie counter in the UI
const cookieCounter = document.getElementById('cookie-counter');
if (cookieCounter) {
cookieCounter.textContent = cookies.length;
}
// Fetch all open tabs
tabs = await browser.tabs.query({});
}
/**
* Fetches tracking sites, cookies, and tabs concurrently
*/
async function fetchData() {
await Promise.all([fetchTrackerDB(), fetchCookiesAndTabs()]);
}
// ==================== SETTINGS MANAGEMENT ====================
/**
* Applies user settings from storage
* Merges stored settings with defaults
*/
async function applySettings() {
const defaultSettings = {
enableGhostIcon: true,
enableSpecialJarIcon: true,
enablePartitionIcon: true,
enableActiveTabHighlight: true,
mycleanerCookies: false,
mycleanerBrowsingHistory: true,
mycleanerCache: false,
mycleanerAutofill: false,
mycleanerDownloadHistory: true,
mycleanerService: false,
mycleanerPlugin: false,
mycleanerLocal: false,
mycleanerIndexed: false,
mycleanerPasswords: false,
OpenTabsTop: false,
showCookieCountBadge: true
};
try {
// Get settings from storage and merge with defaults
const storedSettings = await browser.storage.local.get(defaultSettings);
const settings = { ...defaultSettings, ...storedSettings };
await browser.storage.local.set(settings);
const {
enableGhostIcon,
enableSpecialJarIcon,
enablePartitionIcon,
enableActiveTabHighlight
} = settings;
// Display cookies based on user preferences
displayCookies(enableGhostIcon, enableSpecialJarIcon, enablePartitionIcon);
// Highlight the active tab domain if enabled
if (enableActiveTabHighlight) {
highlightActiveTabDomain();
}
} catch (error) {
console.error('Error applying settings:', error);
}
}
/**
* Initializes the extension by fetching data and applying settings
* Shows a message and auto-closes if no cookies exist
*/
async function initExtension() {
try {
// Load favorites and sniper domains first
await loadFavorites();
await loadSniperDomains();
// Then check if any cookies exist
cookies = await fetchAllCookies();
// If no cookies exist, show message and auto-close
if (!hasCookiesToDelete()) {
showNoCoookiesMessage();
return;
}
// Otherwise, continue with normal initialization
await fetchData();
await applySettings();
} catch (error) {
console.error('Error initializing extension:', error);
}
}
/**
* Displays a message when no cookies are found and auto-closes
*/
function showNoCoookiesMessage() {
// Clear document body content
document.body.innerHTML = '';
document.body.style.display = 'flex';
document.body.style.justifyContent = 'center';
document.body.style.alignItems = 'center';
document.body.style.padding = '20px';
document.body.style.textAlign = 'center';
document.body.style.height = '80px';
document.body.style.width = '200px';
// Create and add the message
const messageElement = document.createElement('div');
messageElement.textContent = 'No cookies found in your browser';
messageElement.style.fontSize = '14px';
messageElement.style.color = 'var(--text-color)';
messageElement.style.fontWeight = '500';
document.body.appendChild(messageElement);
// Auto-close after 2.5 seconds
setTimeout(() => {
window.close();
}, 2500);
}
/**
* Updates the display with current cookies and tabs
* Shows empty message if no cookies remain
*/
async function updateDisplay() {
try {
// Fetch cookies first
cookies = await fetchAllCookies();
// If we now have zero cookies, show message and return
if (cookies.length === 0) {
showNoCoookiesMessage();
return;
}
// Continue with normal display update
tabs = await browser.tabs.query({});
// Update cookie counter
const cookieCounter = document.getElementById('cookie-counter');
if (cookieCounter) {
cookieCounter.textContent = cookies.length;
}
// Get current settings
const settings = await browser.storage.local.get([
'enableGhostIcon',
'enableSpecialJarIcon',
'enablePartitionIcon',
'enableActiveTabHighlight'
]);
displayCookies(
settings.enableGhostIcon,
settings.enableSpecialJarIcon,
settings.enablePartitionIcon
);
// Highlight active tab if enabled
if (settings.enableActiveTabHighlight) {
highlightActiveTabDomain();
}
} catch (error) {
console.error('Error updating display:', error);
}
}
// Initialize the extension when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', initExtension);
// ==================== DISPLAY LOGIC ====================
/**
* Gets the current browser theme (dark or light)
* @returns {string} 'dark' or 'light'
*/
function getCurrentTheme() {
// Cache the result to avoid multiple DOM queries
if (!getCurrentTheme.cache) {
getCurrentTheme.cache = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
// Update the cache when the theme changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
getCurrentTheme.cache = e.matches ? 'dark' : 'light';
});
}
return getCurrentTheme.cache;
}
/**
* Displays cookies with favorites and insight icons
* @param {boolean} enableGhostIcon - Whether to show tracking site icon
* @param {boolean} enableSpecialJarIcon - Whether to show container icon
* @param {boolean} enablePartitionIcon - Whether to show partition icon
*/
async function displayCookies(enableGhostIcon, enableSpecialJarIcon, enablePartitionIcon) {
// Fetch the "OpenTabsTop" setting to determine sorting behavior
const { OpenTabsTop = false } = await browser.storage.local.get('OpenTabsTop');
const container = document.getElementById('cookies-container');
const starDock = document.querySelector('.star-dock');
if (!container) return; // Safety check
// Clear previous content
container.innerHTML = '';
if (starDock) starDock.innerHTML = '';
const fragment = document.createDocumentFragment();
const starsFragment = document.createDocumentFragment();
// Build a Set of main domains from open tabs for quick membership checks
const openTabDomainsSet = new Set(tabs.map(({ url }) => {
try {
return getMainDomain(new URL(url).hostname);
} catch {
return '';
}
}).filter(Boolean));
// Aggregate cookie info by main domain
const domainInfo = {};
cookies.forEach(cookie => {
const mainDomain = getMainDomain(cookie.domain);
if (!domainInfo[mainDomain]) {
domainInfo[mainDomain] = {
count: 0,
hasNonDefaultContainer: false,
hasPartition: false
};
}
domainInfo[mainDomain].count += 1;
if (cookie.storeId !== 'firefox-default') {
domainInfo[mainDomain].hasNonDefaultContainer = true;
}
if (cookie.partitionKey) {
domainInfo[mainDomain].hasPartition = true;
}
});
// Convert domain info object to array for sorting
let domainsArray = Object.entries(domainInfo);
if (OpenTabsTop) {
// Separate domains into those with open tabs and others
const openTabsDomains = [];
const otherDomains = [];
domainsArray.forEach(([domain, info]) => {
if (openTabDomainsSet.has(domain)) {
openTabsDomains.push([domain, info]);
} else {
otherDomains.push([domain, info]);
}
});
// Sort each group alphabetically by domain name
openTabsDomains.sort(([a], [b]) => a.localeCompare(b));
otherDomains.sort(([a], [b]) => a.localeCompare(b));
// Concatenate open tab domains first, then others
domainsArray = [...openTabsDomains, ...otherDomains];
} else {
// Sort all domains alphabetically if setting is disabled
domainsArray.sort(([a], [b]) => a.localeCompare(b));
}
// Create and append DOM elements for each domain entry
domainsArray.forEach(([website, info], index) => {
const element = document.createElement('div');
element.className = 'cookie-item';
element.dataset.index = index;
element.dataset.domain = website;
element.textContent = `${website} (${info.count})`;
element.title = `Left-click to delete all cookies for ${website}; right-click for detailed cookie information; press Command on macOS or Ctrl on PC to use the Tab Switcher function for the open tabs.`;
// Favorite star icon
const star = document.createElement('img');
star.src = favorites.includes(website) ? 'resources/star_full.svg' : 'resources/star_empty.svg';
star.alt = 'Favorite Star Icon';
star.className = 'star-icon';
star.dataset.website = website;
star.dataset.index = index;
star.addEventListener('click', (event) => {
event.stopPropagation();
if (favorites.includes(website)) {
star.src = 'resources/star_empty.svg';
const idx = favorites.indexOf(website);
if (idx !== -1) favorites.splice(idx, 1);
} else {
star.src = 'resources/star_full.svg';
favorites.push(website);
}
saveFavorites(favorites);
});
element.insertBefore(star, element.firstChild);
fragment.appendChild(element);
// Append insight icons based on settings and domain info
if (enableGhostIcon && trackingSites.has(website)) {
appendIcon(element, 'resources/insight_ghost.svg', `${website} tracking icon`);
}
if (enableSpecialJarIcon && info.hasNonDefaultContainer) {
appendIcon(element, 'resources/insight_container.svg', `${website} container icon`);
}
if (enablePartitionIcon && info.hasPartition) {
appendIcon(element, 'resources/insight_partition.svg', `${website} partition icon`);
}
// Highlight domains with open tabs in green color
if (openTabDomainsSet.has(website)) {
element.style.color = '#05A55D';
}
});
container.appendChild(fragment);
if (starDock) starDock.appendChild(starsFragment);
// Position stars aligned with their domain entries after rendering
requestAnimationFrame(() => positionStarsInDock());
// Call highlightActiveTabDomain at the end of displayCookies
const settings = await browser.storage.local.get('enableActiveTabHighlight');
if (settings.enableActiveTabHighlight) {
highlightActiveTabDomain();
}
}
/**
* Helper function to append icon to an element
* @param {HTMLElement} element - The element to add icon to
* @param {string} iconSrc - Source of the icon
* @param {string} altText - Alt text for the icon
*/
function appendIcon(element, iconSrc, altText) {
const icon = document.createElement('img');
icon.src = iconSrc;
icon.alt = altText;
icon.className = 'insight-icon';
element.appendChild(document.createTextNode(' '));
element.appendChild(icon);
}
/**
* Positions stars in the vertical dock aligned with their domain entries
*/
function positionStarsInDock() {
const container = document.getElementById('cookies-container');
const starDock = document.querySelector('.star-dock');
if (!container || !starDock) return;
const stars = starDock.querySelectorAll('.star-icon');
const domainElements = container.querySelectorAll('div');
// Position each star next to its corresponding domain entry
domainElements.forEach((element, index) => {
if (index < stars.length) {
const rect = element.getBoundingClientRect();
const top = element.offsetTop + (rect.height / 2) - 7; // Center vertically
stars[index].style.top = `${top}px`;
}
});
}
/**
* Highlights the domain of the active tab with an icon
*/
function highlightActiveTabDomain() {
const activeTab = tabs.find(tab => tab.active);
if (!activeTab) return;
try {
const activeDomain = getMainDomain(new URL(activeTab.url).hostname);
const container = document.getElementById('cookies-container');
if (!container) return;
// Remove any existing active tab icon
container.querySelector('.active-tab-icon')?.remove();
// Find the element for the active domain
const activeElement = Array.from(container.childNodes).find(element => {
const elementText = element.textContent.trim().split(' ')[0];
return elementText && getMainDomain(elementText) === activeDomain;
});
if (activeElement) {
const icon = document.createElement('img');
icon.src = 'resources/insight_eye.svg';
icon.alt = 'Active Tab Icon';
icon.className = 'insight-icon active-tab-icon';
activeElement.appendChild(document.createTextNode(' '));
activeElement.appendChild(icon);
}
} catch (error) {
console.error('Error highlighting active tab domain:', error);
}
}
// ==================== TAB SWITCHER ====================
/**
* Highlights domains of open tabs more prominently when the platform-specific key is pressed
* @param {boolean} isKeyPressed - Whether the modifier key is pressed
*/
function highlightOpenTabDomains(isKeyPressed) {
const container = document.getElementById('cookies-container');
if (!container) return;
// Cache open tab domains
const openTabDomains = new Set(tabs.map(({ url }) => {
try {
return getMainDomain(new URL(url).hostname);
} catch (e) {
return '';
}
}).filter(Boolean));
// Loop through all domain elements
Array.from(container.children).forEach(element => {
const website = element.textContent.split(' ')[0];
const mainDomain = getMainDomain(website);
if (openTabDomains.has(mainDomain)) {
// Normal appearance (just green text)
element.style.color = '#05A55D';
// Enhanced appearance when key is pressed
if (isKeyPressed) {
element.style.fontWeight = 'bold';
element.style.transform = 'translateX(4px)';
element.style.cursor = 'alias'; // Show 'goto' cursor
} else {
element.style.fontWeight = 'normal';
element.style.transform = '';
element.style.cursor = 'pointer';
}
} else {
element.style.color = ''; // Reset color if not active
}
});
}
/**
* Finds a tab that matches the given domain and activates it
* @param {string} domain - Domain to navigate to
* @returns {Promise<boolean>} - True if a tab was found and activated
*/
async function navigateToTab(domain) {
const mainDomain = getMainDomain(domain);
for (const tab of tabs) {
try {
const tabHostname = new URL(tab.url).hostname;
const tabMainDomain = getMainDomain(tabHostname);
// If domains match, switch to this tab
if (isDomainOrSubdomain(tabMainDomain, mainDomain) ||
isDomainOrSubdomain(mainDomain, tabMainDomain)) {
if (!tab.active) {
await browser.tabs.update(tab.id, { active: true });
await browser.windows.update(tab.windowId, { focused: true });
}
window.close(); // Close popup after navigation
return true;
}
} catch (e) {
// Skip invalid URLs
continue;
}
}
return false;
}
/**
* Determines if the platform-specific modifier key is pressed
* @param {Event} event - The keyboard event
* @returns {boolean} True if the platform-specific modifier key is pressed
*/
function isModifierKeyPressed(event) {
const isMacOS = navigator.platform.toLowerCase().includes('mac');
return isMacOS ? event.metaKey : event.ctrlKey;
}
// ==================== DETAILED TABLE ====================
/**
* Displays detailed cookie information in a table format
* @param {string} mainDomain - The main domain to display cookies for
* @param {Array} cookies - Array of cookie objects
*/
function displayCookieDetails(mainDomain, cookies) {
const isDarkMode = getCurrentTheme() === 'dark';
// Filter and sort cookies for the given main domain
const sortedCookies = cookies.filter(cookie => getMainDomain(cookie.domain) === mainDomain)
.sort((a, b) => a.name.localeCompare(b.name));
const table = document.createElement('table');
table.className = 'cookie-table';
// Define table headers and their descriptions
const headers = [
{ title: 'Name', description: 'The name of the cookie, which is used to identify it when sent between the client and the server.' },
{ title: 'Value', description: 'The value of the cookie, which is the data stored within the cookie.' },
{ title: 'Size', description: 'The size of the cookie in bytes using a function that encodes both the cookie name and value, where each character is assumed to be one byte.' },
{ title: 'Domain', description: 'The domain for which the cookie is valid. The cookie will only be sent to the specified domain and its subdomains.' },
{ title: 'Partition', description: 'Partition attribute for Cookies Having Independent Partitioned State (CHIPS). Without cookie partitioning, third-party cookies can track users across the web. CHIPS, on the other hand, are restricted to the specific site on which they are set, preventing cross-site tracking while still allowing useful functions such as maintaining state across a domain and its subdomains.' },
{ title: 'Container', description: 'The container in which the cookie is stored. Containers (also known as stores or jars) are used to separate cookies and other site data for different contexts or identities, allowing users to manage their online activities and privacy by keeping data from different sites separate. With Firefox Total Cookie Protection now enabled by default, most of your cookies are automatically restricted to the sites that created them, whether you use a specific container or not.' },
{ title: 'Expiration', description: 'The date on which the cookie will expire. A session cookie is a type of cookie that does not have an expiration date set. These cookies are stored in temporary memory and are deleted when closing the browser. Persistent cookies have an expiration date and are stored on the device until they expire or are explicitly deleted.' },
{ title: 'Secure', description: 'When this flag is set, the cookie will only be sent over secure (HTTPS) connections.' },
{ title: 'HttpOnly', description: 'When this flag is set, the cookie is not accessible via JavaScript.' },
{ title: 'SameSite', description: 'This attribute controls when the cookie will be sent in cross-site requests. It can be set to Strict, Lax, or None. Strict means the cookie will only be sent in a first-party context, Lax restricts the cookie to top-level navigation and safe HTTP methods, and None means the cookie will be sent in all contexts.' },
{ title: '', description: '' }
];
// Create header row
const headerRow = table.insertRow();
headers.forEach(({ title, description }) => {
const headerCell = document.createElement('th');
headerCell.textContent = title;
headerCell.title = description;
headerCell.className = 'header-cell';
headerRow.appendChild(headerCell);
});
const sameSiteMap = {
'no_restriction': 'None',
'lax': 'Lax',
'strict': 'Strict'
};
/**
* Adds a row for each cookie in the table
* @param {Object} cookie - Cookie object
*/
const addRow = (cookie) => {
const row = table.insertRow();
const { name, value, domain, partitionKey, storeId, expirationDate, secure, httpOnly, sameSite, path } = cookie;
const cookieSize = calculateCookieSize(cookie);
const formattedStoreId = storeId.replace(/^firefox-/, '');
const expirationDateFormatted = formatExpirationDate(expirationDate);
const partitionValue = partitionKey?.topLevelSite || '';
// Check if the cookie is a favorite
const isFavorite = favorites.includes(getMainDomain(cookie.domain));
if (isFavorite) {
row.style.opacity = '0.5';
}
// Prepare cell contents for the row
const cellContents = [
name,
value,
cookieSize,
domain,
partitionValue,
formattedStoreId,
expirationDateFormatted || 'Session',
secure ? 'Yes' : 'No',
httpOnly ? 'Yes' : 'No',
sameSiteMap[sameSite] || 'None',
'✎' // Edit icon
];
// Create and append cells to the row
cellContents.forEach((content, index) => {
const cell = document.createElement('td');
cell.className = 'cell';
if (index === cellContents.length - 1) {
// This is the edit cell
cell.textContent = content;
cell.title = 'Edit this cookie. It is a BETA version so proceed with caution.';
cell.style.cursor = 'pointer';
cell.style.textAlign = 'center';
cell.style.fontSize = '16px';
cell.addEventListener('click', (event) => {
event.stopPropagation();
const cookieData = {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
partition: partitionValue,
container: formattedStoreId,
expiration: expirationDateFormatted || '',
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite || 'None',
};
const queryParams = new URLSearchParams();
queryParams.set('data', encodeURIComponent(JSON.stringify(cookieData)));
browser.tabs.create({ url: `create.html?${queryParams}` });
});
} else {
cell.textContent = content;
// Add tooltip to show full content on hover
cell.addEventListener('mouseenter', function() {
if (!cell.title) {
cell.title = cell.textContent;
}
});
}
// Check if this is the expiration date cell and if the cookie is expired
if (index === 6 && expirationDate) {
const now = Math.floor(Date.now() / 1000);
if (expirationDate < now) {
cell.style.color = 'red';
cell.title = 'This cookie has expired.';
}
}
row.appendChild(cell);
});
// Add event listeners for the row
row.addEventListener('click', async (event) => {
event.stopPropagation();
if (isFavorite) {
return; // Skip deletion for favorites
}
// Store the deleted cookie for undo functionality
tempDeletedCookies.push({ ...cookie });
await deleteCookie({
name: cookie.name,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
storeId: cookie.storeId,
partitionKey: cookie.partitionKey
});
row.remove();
showUndoIcon(); // Show undo option after deletion
});
// Add hover effects for the row
row.addEventListener('mouseenter', function() {
row.style.backgroundColor = isDarkMode ? '#5F5E68' : '#DFDFE4';
row.style.cursor = 'pointer';
});
row.addEventListener('mouseleave', function() {
row.style.backgroundColor = '';
row.style.cursor = 'default';
});
};
sortedCookies.forEach(addRow);
document.body.appendChild(table);
}
// ==================== COOKIE DELETION ====================
/**
* Deletes a cookie based on its properties
* @param {Object} cookie - Cookie object with properties needed for deletion
* @returns {Promise<void>}
*/
async function deleteCookie(cookie) {
const isFavorite = favorites.includes(getMainDomain(cookie.domain));
if (isFavorite) return; // Skip deletion for favorites
const cookieUrl = getCookieUrl(cookie);
const storeId = cookie.storeId || '0';
try {
await browser.cookies.remove({
url: cookieUrl,
name: cookie.name,
storeId: storeId,
partitionKey: cookie.partitionKey
});
// Fetch cookies again to see if we've removed the last one
cookies = await fetchAllCookies();
// If no cookies left, show message and return
if (cookies.length === 0) {
showNoCoookiesMessage();
return;
}
// Otherwise continue with normal update
await updateDisplay();
} catch (error) {
console.error('Error deleting cookie:', error);
}
}
/**
* Deletes cookies based on a filter function
* @param {Function} filterFn - Function that returns true for cookies to delete
* @returns {Promise<void>}
*/
async function deleteCookies(filterFn) {
try {
const cookiesToDelete = cookies.filter(filterFn);
await Promise.all(cookiesToDelete.map(deleteCookie));
} catch (error) {
console.error('Error deleting multiple cookies:', error);
}
}
/**
* Deletes all cookies for a specific domain
* @param {string} domain - Domain to delete cookies for
*/
async function deleteAllCookiesForDomain(domain) {
if (favorites.includes(getMainDomain(domain))) return; // Skip deletion for favorites
try {
const domainCookies = cookies.filter(cookie =>
getMainDomain(cookie.domain) === getMainDomain(domain)
);
// Save for potential undo
tempDeletedCookies = domainCookies.map(cookie => ({ ...cookie }));
// Delete all cookies for this domain
await deleteCookies(cookie =>
getMainDomain(cookie.domain) === getMainDomain(domain)
);
// Fetch cookies again to see if we've removed everything
cookies = await fetchAllCookies();
// If no cookies left, show message and return
if (cookies.length === 0) {
showNoCoookiesMessage();
return;
}
// Otherwise show undo icon and update display
showUndoIcon();
await updateDisplay();
} catch (error) {
console.error('Error deleting cookies for domain:', error);
}
}
/**
* Deletes cookies from tabs that are no longer open
* @param {Array} closedTabsCookies - Array of cookies from closed tabs
*/
async function deleteCookiesFromClosedTabs(closedTabsCookies) {
try {
await deleteCookies(cookie => {
return closedTabsCookies.includes(cookie) &&
!favorites.includes(getMainDomain(cookie.domain));
});
// Fetch cookies again to see if we've removed everything
cookies = await fetchAllCookies();
// If no cookies left, show message and return
if (cookies.length === 0) {
showNoCoookiesMessage();
return;
}
} catch (error) {
console.error('Error deleting cookies from closed tabs:', error);
}
}
/**
* Deletes cookies matching sniper domain list (exact or wildcard)
* Exact: "google.com" only matches google.com
* Wildcard: "*google.com" matches any subdomain of google.com
* Respects favorite domains
*/
async function mySniper() {
if (sniperDomains.length === 0) return;
try {
const favoriteDomains = new Set(favorites.map(getMainDomain));
// For each sniper domain, delete matching cookies
for (const sniperDomain of sniperDomains) {
const isWildcard = sniperDomain.startsWith('*');
const cleanDomain = isWildcard ? sniperDomain.substring(1) : sniperDomain;
const cookiesToDelete = cookies.filter(cookie => {
const mainDomain = getMainDomain(cookie.domain);
// Skip if domain is in favorites
if (favoriteDomains.has(mainDomain)) return false;
if (isWildcard) {
// Wildcard: match cleanDomain and all subdomains
return cookie.domain === cleanDomain ||
cookie.domain === '.' + cleanDomain ||
cookie.domain.endsWith('.' + cleanDomain);
} else {
// Exact: match cleanDomain only
return cookie.domain === cleanDomain ||
cookie.domain === '.' + cleanDomain;
}
});
for (const cookie of cookiesToDelete) {
await deleteCookie(cookie);
}
}
// Fetch cookies again to update display
cookies = await fetchAllCookies();
if (cookies.length === 0) {
showNoCoookiesMessage();
return;
}
await updateDisplay();
} catch (error) {
console.error('Error running mySniper:', error);
}
}
/**
* Undoes the last cookie deletion
* Restores all cookies from tempDeletedCookies array
*/