Saturday, April 7, 2012

Deleting Safari data

I've been annoyed with Safari for a while now because it's so difficult to completely remove all website data. These items include not only cookies but also the Cache and Local Storage.

It's not unusual to do Preferences > Privacy > Remove All Website Data.., only to see some of the cookies come back. One solution is to zap the data, quit the application and then re-open it, and zap again. It also helps if you do a reset first: Safari > Reset Safari.. > Reset. I do this with "Remove saved names and passwords" unchecked.

One solution would be to do private browsing (Safari > Private Browsing.. > OK), but sometimes you want cookies for interaction with a few websites.

A stereotyped sequence of actions---that sounds like a candidate for a script. The default scriptable items in Safari are pretty minimal: drag-and-drop the Safari application icon onto the Applescript Editor icon in Utilities to see. (I just noticed that it looks as though these can be extended by GUI scripting. So that might be solution 2 to the problem, I'll have to check it out.)

A third solution is developed here. It's not perfect, mainly because in Lion the previous state of the browser is restored, reloading pages and their cookies. But I think it's interesting in combining some different techniques. I got a start on this from a Q & A on Stack Exchange.

The Python script (listing at the end below) consists of five functions (calling a sixth), and then a seventh that calls the others:

def reset():
    close_windows()
    close_app('Safari')
    eat_cookies()
    p = '/Library/Caches/com.apple.Safari/Cache.db'
    remove_data(p)
    p = '/Library/Safari/LocalStorage'
    remove_data(p, is_dir=True)
    open_app('Safari')
    
reset()

The function home returns the users home directory. We could do this just staying with Python:

>>> import os
>>> os.path.expanduser(os.getcwd())
'/Users/telliott/Desktop'

but this hasn't worked quite as you'd expect. Instead, in the script we get the HOME environment variable using NSProcessInfo. We'll use this to build the paths for the Cache and Local Storage data.

In close_windows, we use Applescript. We write the script to a temporary (hidden) file, execute it from the shell, and afterwards remove the script file. This part doesn't act as I'd like, because even though it works, Safari will reload the window, its URL and their cookies when we re-start the app.

The function open_app just calls open -a Safari from the shell.

The function close_app can't do close from the shell, as there is no such command. My solution was to use ps, followed by a filter on active processes for the string "Safari" (checking that there is at most one result) and then killing that process if it's running. [ UPDATE: I modified the script to first check if Safari is running, and only do the close and open if it is. ]

The function remove_data checks to see if a path exists, and if it does, removes the file. It can be called with is_dir=True to do so recursively.

Finally, eat_cookies uses NSHTTPCookieStorage.sharedHTTPCookieStorage. I explored this in the interpreter, screening dir(store) until I came across deleteCookie_.

In summary, we use Applescript to close all Safari windows, the shell to quit and re-open the app, a Foundation API to remove the cookies, and standard file deletion functions to remove the Cache and Local Storage. The only problem is if there's a page or set of tabs open when we do this, they come back, along with their cookies.

Finally, a note about NSURLCache as mentioned in the answer on StackExchange.

c = NSURLCache.sharedURLCache
c.removeAllCachedResponses()

doesn't work, because it seems that most applications construct their own NSURLCache.

import os, sys, time, subprocess
from subprocess import Popen, PIPE
import Foundation

def home():
    obj = Foundation.NSProcessInfo
    D = obj.processInfo().environment()
    return D['HOME']
    
def get_pid(app):
    cmd = 'ps -ax | grep ' + app
    p = Popen(cmd,shell=True,
              stdout=PIPE,close_fds=True)
    result = p.stdout.read()
    L = result.strip().split('\n')
    L = [e for e in L if app + '.app' in e]
    if not(L):  return None
    assert len(L) == 1
    pid = L[0].split()[0]
    return pid

def close_windows():
    s = '''
    tell application "Safari"
        close windows
    end tell
    '''
    script_path = home() + '/.x.scpt'
    FH = open(script_path, 'w')
    FH.write(s)
    FH.close()
    cmd = 'osascript ' + script_path
    result = subprocess.call(cmd,shell=True)
    cmd = 'rm ' + script_path
    result = subprocess.call(cmd,shell=True)
        
def open_app(app):
    cmd = 'open -a ' + app
    result = subprocess.call(cmd,shell=True)
    
def close_app(pid):
    subprocess.call('kill ' + pid,shell=True)
    
def remove_data(path, is_dir=False):
    path = home() + path
    try:
        os.stat(path)
    except OSError:
        return
    cmd = 'rm '
    if is_dir:
        cmd += '-r '
    cmd += path
    result = subprocess.call(cmd,shell=True)
        
def eat_cookies(v=True):
    obj = Foundation.NSHTTPCookieStorage
    store = obj.sharedHTTPCookieStorage()
    L = store.cookies()
    L = [store.deleteCookie_(c) for c in L]
    if v:
        print 'deleted ', len(L) - len(store.cookies()),
        print 'cookies'

def reset():
    app = 'Safari'
    pid = get_pid(app)
    if pid:
        close_windows()
        close_app(pid)
    eat_cookies()
    p = '/Library/Caches/com.apple.Safari/Cache.db'
    remove_data(p)
    p = '/Library/Safari/LocalStorage'
    remove_data(p, is_dir=True)
    if pid:
        open_app('Safari')
    
reset()