Use Safari Cookie Whitelist to save specific cookies, while deleting all others?

On Mac OS Big Sur I had been working on a script to save / remove particular cookies. Trying to avoid using System Events / Accessibility, I was working on manipulating the cookie file directly. After migrating to Ventura, the cookies file is squirreled away, so I’ve reverted back to a System Events / Accessibility script. I’d like to create a Safari cookie whitelist, then scroll through the entire list of cookies and delete any cookie not found on my whitelist. The problem is that I can’t copy the name of each cookie from the “Settings > Privacy > Manage Website Data…” dialog. Any suggestions on how I might do that — or an alternate method to achieve the same result?

The following script isn’t exactly what I’m trying to do, but if I could get cookie names, I could selectively delete them.

tell application "System Events"

tell application process "Safari"
	
	tell application "Safari" to activate
	
	click menu item "Settings…" of menu "Safari" of menu bar item "Safari" of menu bar 1
	
	click button "Privacy" of toolbar 1 of window 1
	
	tell window 1
		
		click button "Manage Website Data…" of group 1 of group 1
		
		delay 3 --> ADJUST AS NEEDED TO ALLOW COOKIES TO LOAD.
		
		tell sheet 1
			
			select (row 4 of table 1 of scroll area 1)
			
		end tell
		
		tell sheet 1
			
			click button "Remove"
			
		end tell
		
	end tell
	
end tell

end tell

Hi @Rob.

There was a discussion about this on MacScripter a few years ago which you may find useful. I’m still using a script based on my contribution in post #12 of that topic, which I last updated there in September 2020. I’ll post the version I currently use below, which works (or has worked!) with every version of Safari I’ve had since 11.1.2. Unfortunately, I don’t know if it works with whatever version you’ve got in Big Sur as I jumped straight from Mojave to Ventura. But the GUI scripting difference depend on the Safari version rather than the macOS version. It currently works with Safari 17.1.2 in both Ventura and Sonoma. The GUI differences are all to do with the contents of the “Manage Website Data…” sheet.

Another difference between the script below and the one in the MacScripter topic is that I now keep the script object containing the black and white lists as a separate script file which the main script loads when it runs. As noted in my MacScripter post, the script goes through a bit of a performance closing and reopening Safari to make sure the cookie database is updated before any edits. Hope this helps.

(* Open the "Manage Website Data" sheet in Safari's Preferences window, select blacklisted stored-data domains, and, if set to do so, delete them.

This has only been tested in Engish with Safari 11.1.2 in El Capitan, in English, French, and Greek with Safari 12.0.3 in Mojave, and in English in Safari 12.1 - 13.1 in Mojave.
Assumptions are kept to a minimum, but it's been necessary to assume that:
	• The first item with an associated keystroke in Safari's "Safari" menu is "Preferences…" in the local language.
	• The "Privacy" button is the seventh in the Preferences window toolbar.
	• The "Privacy" pane is the only one with a certain number of UI elements in group 1 of group 1. (The actual number depends on the Safari version.)
	• The first *named* button in the "Privacy" group is "Manage Website Data…" in the local language.
	• The seventh item with an associated keystroke in Safari's "Edit" menu is "Select All" in the local language.
	• The first button in the drop-down sheet is the "Remove" button and the last is "Done".
*)

use AppleScript version "2.5" -- Mac OS 10.11 (El Capitan) or later (for Safari 11.1.2 or later).
use scripting additions

main()

on main()
	-- Script containing various customising properties for convenience. Now kept and maintained externally.
	set scriptObject to (load script file ((path to scripts folder as text) & "Libraries:Safari CookieCleaner Lists.scpt"))
	
	set SafariWasOpen to (application id "com.apple.Safari" is running)
	-- If Safari's running, close it to get rid of any open sites or invisible "closed" windows and to update its stored-data list.
	if (SafariWasOpen) then closeSafari()
	-- Open the stored-data sheet in the "Privacy" pane of Safari's preferences.
	openWebsiteDataSheet()
	-- Select blacklisted domains in the data and remove them if set to do so.
	manageWebsiteData(scriptObject)
	(*
	-- Close Safari if it wasn't open before. (DISABLED TO ALLOW SAFARI TIME TO COMPLETE THE DISK WORK.)
	if (not SafariWasOpen) then closeSafari()
	*)
end main

(* Close Safari to get rid of any open sites or invisible "closed" windows. *)
on closeSafari()
	tell application id "com.apple.systemevents"
		tell (first application process whose bundle identifier is "com.apple.Safari")
			set frontmost to true
			-- If a drop-down sheet is open in the Preferences window, dismiss it.
			tell sheet 1 of window 1
				if (it exists) then
					click last button
					repeat while (it exists)
						delay 0.5
					end repeat
				end if
			end tell
			-- Click "Quit Safari" in the "Safari" menu and wait for the Safari process to disappear. This works much faster here than a scripted 'quit' command to the application!
			-- (NB. menu item -2. menu item -1 of the "Safari" menu is "Quit and Keep Windows", which appears instead of "Quit Safari" when the option key's held down.)
			click menu item -2 of menu 1 of second menu bar item of menu bar 1
			repeat while (it exists)
				delay 0.5
			end repeat
		end tell
	end tell
end closeSafari

(* Open the stored-data sheet in the "Privacy" pane of Safari's preferences. *)
on openWebsiteDataSheet()
	-- (Re)open Safari.
	tell application id "com.apple.Safari"
		activate
		set windowCount to (count windows) -- Safari itself only counts its document windows
		set SafariVersion to version
	end tell
	
	-- The number of UI elements in the Privacy pane depends on the Safari version.
	-- We'll need to know how many to expect in group  1 of group 1 so that we know when that pane is showing.
	set PrivacyButtonCount to 2
	considering numeric strings
		if ((SafariVersion < "11") or (SafariVersion is not less than "18.0")) then
			tell application id "com.apple.Safari" to display dialog ("This script's not known to work with Safari " & SafariVersion & "!") buttons {"Stop"} default button 1 cancel button 1 with icon stop
		else if (SafariVersion begins with "11") then
			set PrivacyElementCount to 9 -- Tested with Safari 11.1.2.
		else if (SafariVersion begins with "12.0") then
			set PrivacyElementCount to 7 -- Tested with Safari 12.0.3.
		else if (SafariVersion < "14.1") then
			set PrivacyElementCount to 6 -- Tested with Safari 12.1, Safari 13.x, Safari 14.0.
		else if (SafariVersion < "16.6") then
			set PrivacyElementCount to 8 -- Tested with Safari 14.1.
		else
			set PrivacyElementCount to 12 -- Tested with Safari 16.6. Still works with Safari 17.1.2.
			if (SafariVersion < "17.0") then
				set PrivacyButtonCount to 3
			else
				set PrivacyButtonCount to 4 -- Safari 17.1.
			end if
		end if
	end considering
	
	tell application id "com.apple.systemevents"
		tell (first process whose bundle identifier is "com.apple.Safari")
			set frontmost to true
			
			-- Click the first menu item in the "Safari" menu which has a keyboard shortcut. This is hopefully "Preferences…" in the local language.
			set SafariMenu to menu 1 of menu bar item 2 of menu bar 1
			click (first menu item of SafariMenu where class of value of attribute "AXMenuItemCmdChar" is not missing value)
			
			-- Delay until the number of Safari windows is more than the number of its document windows.
			repeat until ((count windows) > windowCount)
				delay 0.1
			end repeat
			-- Use a 'tell' statement with an index reference to the window. (The window's name may change when the "Privacy" button's clicked.)
			tell window 1
				-- Cllck the seventh button in the window's toolbar, delay until there are PrivacyElementCount UI elements in group 1 of group 1 and two of them are buttons, then click the first named button in that group.
				click button 7 of toolbar 1
				tell group 1 of group 1
					repeat until (((count UI elements) is PrivacyElementCount) and ((count buttons) is PrivacyButtonCount))
						delay 0.1
					end repeat
					click button (first text of (get name of buttons))
				end tell
				
				-- Wait for the drop-down sheet to appear.
				repeat until (sheet 1 exists)
					delay 0.1
				end repeat
				-- Then wait for the "Loading Website Data…" message to disappear. This may take some time, depending on processor activity.)
				repeat while ((value of static text 1 of sheet 1 ends with "…") or (value of static text -1 of sheet 1 ends with "…"))
					delay 0.5
				end repeat
			end tell
		end tell
	end tell
end openWebsiteDataSheet

(* Select blacklisted domains in the stored-data sheet and remove them if set to do so. *)
on manageWebsiteData(scriptObject)
	tell application "Safari" to set SafariVersion to version
	
	-- Get a reference to the table displaying the stored-data domains and count the listed domains.
	tell application id "com.apple.systemevents"
		set appProcessSafari to first application process whose bundle identifier is "com.apple.Safari"
		set domainTable to table 1 of scroll area 1 of sheet 1 of window 1 of appProcessSafari
		set rowCount to (count rows of domainTable)
	end tell
	
	-- If the table's not empty:
	if (rowCount > 0) then
		tell application id "com.apple.systemevents"
			-- Switch the focus to the table.
			set focused of domainTable to true
			-- Select the table's entire contents by clicking the seventh menu item with a keyboard shortcut (hopefully "Select All") in Safari's "Edit" menu.
			set EditMenu to menu 1 of menu bar item 4 of menu bar 1 of appProcessSafari
			click (seventh menu item of EditMenu where class of value of attribute "AXMenuItemCmdChar" is not missing value)
			-- Get the description of UI element 1 of every row in the table. (Changed in Safari 13.1. It's now the elements' names which match the domains.)
			considering numeric strings
				if (SafariVersion < "13.1") then
					set scriptObject's rowDescriptions to description of UI element 1 of rows of domainTable
				else
					set scriptObject's rowDescriptions to name of UI element 1 of rows of domainTable
				end if
			end considering
		end tell
		
		-- Except for "Local documents on your computer", each description consists of a domain name followed by a space and the kind(s) of data stored.
		-- We'll use this first space to parse the domain names.
		set astid to AppleScript's text item delimiters
		set AppleScript's text item delimiters to space
		set patternCount to (count scriptObject's blackPatterns)
		-- Work through the descriptions, counting the rows corresponding to blacklisted domains and unselecting the others. This may also take a while!
		if (rowCount > 7) then say "Please wait for the next announcement."
		set n to 0
		repeat with i from 1 to rowCount
			set thisDescription to item i of scriptObject's rowDescriptions
			set thisDomain to text item 1 of thisDescription
			if (scriptObject's blackingAllButWhiteList) then
				-- Blacklisting everything not in the white list. Unselect the row if this domain's in the white list.
				if (thisDomain is in scriptObject's whiteList) then
					tell application id "com.apple.systemevents" to set selected of row i of domainTable to false
				else
					set n to n + 1
				end if
			else if (thisDomain is in scriptObject's blackList) then
				-- Heeding the blacklisting and this domain's explicitly blacklisted. Leave the row selected.
				set n to n + 1
			else if (thisDomain is in scriptObject's whiteList) then
				-- Heeding the blacklisting and this domain's explicitly whitelisted. Unselect the row.
				tell application id "com.apple.systemevents" to set selected of row i of domainTable to false
			else
				-- Heeding the blacklisting and no explicit match either way. Test the domain against the wildcard patterns and unselect the row if no matches.
				repeat with j from 1 to patternCount
					set thisPattern to item j of scriptObject's blackPatterns
					if (thisPattern begins with "*") then
						if (thisPattern ends with "*") then
							set zapping to (thisDomain contains text 2 thru -2 of thisPattern)
						else
							set zapping to (thisDomain ends with text 2 thru -1 of thisPattern)
						end if
					else -- if (thisDomain ends with "*") then
						set zapping to (thisDomain begins with text 1 thru -2 of thisPattern)
					end if
					if (zapping) then exit repeat
				end repeat
				if (zapping) then
					set n to n + 1
				else
					tell application id "com.apple.systemevents" to set selected of row i of domainTable to false
				end if
			end if
		end repeat
		set AppleScript's text item delimiters to astid
	end if
	
	-- When done, either click the "Remove" button or not and alert the user.
	tell application id "com.apple.systemevents"
		set RemoveButton to button 1 of sheet 1 of window 1 of appProcessSafari
		set RemoveButtonEnabled to RemoveButton's enabled
	end tell
	if (RemoveButtonEnabled) then
		if (scriptObject's deleting) then
			tell application id "com.apple.systemevents"
				set focused of RemoveButton to true
				click RemoveButton
				repeat while ((count rows of domainTable) is rowCount)
					delay 0.1
				end repeat
				-- Click "Done" too (if required).
				(* tell window 1 of appProcessSafari
					set focused of button -1 of sheet 1 to true
					click button -1 of sheet 1
					repeat while (sheet 1 exists)
						delay 0.1
					end repeat
				end tell *)
			end tell
			say (n as text) & " blacklisted domains deleted."
		else
			say (n as text) & " blacklisted domains selected."
		end if
	else
		say "No blacklisted domains found."
	end if
end manageWebsiteData

Hey Nigel,

Thanks so much! I should have checked Macscripter, I usually do. I saw that Ron’s script solved my initial question about getting the cookie names:

set uiChildren to entire contents of sheet 1

I thought that in itself was great, but it also creates the problem of parsing the data.

Your script is so much better! I haven’t tried it yet, but just reading through it, I saw so many bits of great code. I’m really looking forward to setting it up and learning from it as well.

Thanks again!

Rob

Hi Nigel,

I ran your latest script with the white / black list in a separate script file. I had a number of issues that highlight why I love to hate AppleScript — working code from one system fails on a different system. The 1st two errors that I had to correct was for the PrivacyElementCount and PrivacyButtonCount variables.

	--> I HAD TO HARD CODE THESE 2 VARS.
set PrivacyElementCount to 15
set PrivacyButtonCount to 3

Then the whiteList variable seemed to be passed from the external script while the blackList variable failed and caused a cascading number of errors. The easiest solution was to add the external script back to the main script. With the exception of hard coding PrivacyElementCount & PrivacyButtonCount, your script works beautifully.

Here’s what I’m running the script on:

2018 Mac Mini
3.6 GHz Quad-Core Intel Core i3
8 GB 2667 MHz DDR4
Mac OS 13.4.1 (c) (22F770820d)
Safari Version 16.5.2 (18615.2.9.11.10)

Thanks again!

Nigel — I forgot to ask, what is the advantage to having the blackList and blackPatterns?

Hi Rob.

Thanks for the feedback. I’m glad you managed to get the script working on your system.

I’m not altogether surprised that you had to change the PrivacyElementCount and PrivacyButtonCount values. As I mentioned above, I skipped a few macOS versions and may not have received your version of Safari. Or I may have carelessly edited those settings out of the script while updating it at some later point. Or maybe we have a different Preferences setting somewhere that causes the Privacy pane to be displayed differently on our machines. That’s the joy of GUI Scripting. :crazy_face:

I don’t know what the problem could have been with using an external script instead of the script object. The external script has to be a compiled one and all six of the original properties are required, but otherwise there should have been nothing to go wrong. :thinking:

blackList contains complete domain names, blackPatterns contains partial domain names beginning and/or ending with asterisk wildcards. The modus operandi (if I’ve re-studied the code correctly!) is that that all the domains start off “blacked” because it’s easy to select them all in the table with one click in the GUI. Then, if you have the script object’s blackingAllButWhiteList property set to false:
if the full name of a cookie domain is in blackList, it’s left blacked
otherwise
if the full name’s in whiteList, it’s unblacked
otherwise
if it matches any of the wildcard patterns in blackPatterns, it’s left blacked
otherwise
it’s unblacked

It think I reasoned this to be a good approach for speed and usability. Checking for a name in a list and not unblacking it in the GUI takes the least amount of time, so try that first. Next comes checking another list and either unblacking or not. Working through the individual items in a third list looking for a pattern match and either unblacking or not takes longest of all, so leave that until last in the hope that it won’t be needed. It also allows specific domains to be whitelisted which might otherwise be matched by one of the patterns. It gives me a headache just remembering it!

Nigel — Thank you for the explanations, it all makes perfect sense! I like the patterns checking for adding additional whitelist items, I’m going to check that out.