mirror of
https://github.com/samsonjs/http-cookie.git
synced 2026-04-27 14:57:46 +00:00
Make MozillaStore#close actually "work" by closing open statements.
Add a finalizer to MozillaStore also, which automatically closes the SQLite3 database.
This commit is contained in:
parent
db58d2c8ab
commit
ded02f8327
2 changed files with 194 additions and 115 deletions
|
|
@ -9,6 +9,7 @@ class HTTP::CookieJar
|
||||||
# Session cookies are stored separately on memory and will not be
|
# Session cookies are stored separately on memory and will not be
|
||||||
# stored persistently in the SQLite3 database.
|
# stored persistently in the SQLite3 database.
|
||||||
class MozillaStore < AbstractStore
|
class MozillaStore < AbstractStore
|
||||||
|
# :stopdoc:
|
||||||
SCHEMA_VERSION = 5
|
SCHEMA_VERSION = 5
|
||||||
|
|
||||||
def default_options
|
def default_options
|
||||||
|
|
@ -32,6 +33,41 @@ class HTTP::CookieJar
|
||||||
appId inBrowserElement
|
appId inBrowserElement
|
||||||
]
|
]
|
||||||
|
|
||||||
|
SQL = {}
|
||||||
|
|
||||||
|
Callable = proc { |obj, meth, *args|
|
||||||
|
proc {
|
||||||
|
obj.__send__(meth, *args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Database < SQLite3::Database
|
||||||
|
def initialize(file, options = {})
|
||||||
|
@stmts = []
|
||||||
|
options = {
|
||||||
|
:results_as_hash => true,
|
||||||
|
}.update(options)
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare(sql)
|
||||||
|
case st = super
|
||||||
|
when SQLite3::Statement
|
||||||
|
@stmts << st
|
||||||
|
end
|
||||||
|
st
|
||||||
|
end
|
||||||
|
|
||||||
|
def close
|
||||||
|
return self if closed?
|
||||||
|
@stmts.reject! { |st|
|
||||||
|
st.closed? || st.close
|
||||||
|
}
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# :startdoc:
|
||||||
|
|
||||||
# Generates a Mozilla cookie store. If the file does not exist,
|
# Generates a Mozilla cookie store. If the file does not exist,
|
||||||
# it is created. If it does and its schema is old, it is
|
# it is created. If it does and its schema is old, it is
|
||||||
# automatically upgraded with a new schema keeping the existing
|
# automatically upgraded with a new schema keeping the existing
|
||||||
|
|
@ -59,8 +95,14 @@ class HTTP::CookieJar
|
||||||
@filename = options[:filename] or raise ArgumentError, ':filename option is missing'
|
@filename = options[:filename] or raise ArgumentError, ':filename option is missing'
|
||||||
|
|
||||||
@sjar = HashStore.new
|
@sjar = HashStore.new
|
||||||
@db = SQLite3::Database.new(@filename)
|
|
||||||
@db.results_as_hash = true
|
@db = Database.new(@filename)
|
||||||
|
|
||||||
|
@stmt = Hash.new { |st, key|
|
||||||
|
st[key] = @db.prepare(SQL[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectSpace.define_finalizer(self, Callable[@db, :close])
|
||||||
|
|
||||||
upgrade_database
|
upgrade_database
|
||||||
|
|
||||||
|
|
@ -126,6 +168,13 @@ class HTTP::CookieJar
|
||||||
SQL
|
SQL
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def db_prepare(sql)
|
||||||
|
st = @db.prepare(sql)
|
||||||
|
yield st
|
||||||
|
ensure
|
||||||
|
st.close if st
|
||||||
|
end
|
||||||
|
|
||||||
def upgrade_database
|
def upgrade_database
|
||||||
loop {
|
loop {
|
||||||
case schema_version
|
case schema_version
|
||||||
|
|
@ -138,19 +187,18 @@ class HTTP::CookieJar
|
||||||
when 2
|
when 2
|
||||||
@db.execute("ALTER TABLE moz_cookies ADD baseDomain TEXT")
|
@db.execute("ALTER TABLE moz_cookies ADD baseDomain TEXT")
|
||||||
|
|
||||||
st_update = @db.prepare("UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id")
|
db_prepare("UPDATE moz_cookies SET baseDomain = :baseDomain WHERE id = :id") { |st_update|
|
||||||
|
|
||||||
@db.execute("SELECT id, host FROM moz_cookies") { |row|
|
@db.execute("SELECT id, host FROM moz_cookies") { |row|
|
||||||
domain_name = DomainName.new(row['host'][/\A\.?(.*)/, 1])
|
domain_name = DomainName.new(row['host'][/\A\.?(.*)/, 1])
|
||||||
domain = domain_name.domain || domain_name.hostname
|
domain = domain_name.domain || domain_name.hostname
|
||||||
st_update.execute(:baseDomain => domain, :id => row['id'])
|
st_update.execute(:baseDomain => domain, :id => row['id'])
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@db.execute("CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")
|
@db.execute("CREATE INDEX moz_basedomain ON moz_cookies (baseDomain)")
|
||||||
self.schema_version += 1
|
self.schema_version += 1
|
||||||
when 3
|
when 3
|
||||||
st_delete = @db.prepare("DELETE FROM moz_cookies WHERE id = :id")
|
db_prepare("DELETE FROM moz_cookies WHERE id = :id") { |st_delete|
|
||||||
|
|
||||||
prev_row = nil
|
prev_row = nil
|
||||||
@db.execute(<<-'SQL') { |row|
|
@db.execute(<<-'SQL') { |row|
|
||||||
SELECT id, name, host, path FROM moz_cookies
|
SELECT id, name, host, path FROM moz_cookies
|
||||||
|
|
@ -161,6 +209,7 @@ class HTTP::CookieJar
|
||||||
end
|
end
|
||||||
prev_row = row
|
prev_row = row
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@db.execute("ALTER TABLE moz_cookies ADD creationTime INTEGER")
|
@db.execute("ALTER TABLE moz_cookies ADD creationTime INTEGER")
|
||||||
@db.execute("UPDATE moz_cookies SET creationTime = (SELECT id WHERE id = moz_cookies.id)")
|
@db.execute("UPDATE moz_cookies SET creationTime = (SELECT id WHERE id = moz_cookies.id)")
|
||||||
|
|
@ -192,14 +241,15 @@ class HTTP::CookieJar
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def db_add(cookie)
|
SQL[:add] = <<-'SQL' % [
|
||||||
@st_add ||=
|
INSERT OR REPLACE INTO moz_cookies (%s) VALUES (%s)
|
||||||
@db.prepare('INSERT OR REPLACE INTO moz_cookies (%s) VALUES (%s)' % [
|
SQL
|
||||||
ALL_COLUMNS.join(', '),
|
ALL_COLUMNS.join(', '),
|
||||||
ALL_COLUMNS.map { |col| ":#{col}" }.join(', ')
|
ALL_COLUMNS.map { |col| ":#{col}" }.join(', ')
|
||||||
])
|
]
|
||||||
|
|
||||||
@st_add.execute({
|
def db_add(cookie)
|
||||||
|
@stmt[:add].execute({
|
||||||
:baseDomain => cookie.domain_name.domain || cookie.domain,
|
:baseDomain => cookie.domain_name.domain || cookie.domain,
|
||||||
:appId => @app_id,
|
:appId => @app_id,
|
||||||
:inBrowserElement => @in_browser_element ? 1 : 0,
|
:inBrowserElement => @in_browser_element ? 1 : 0,
|
||||||
|
|
@ -217,9 +267,7 @@ class HTTP::CookieJar
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def db_delete(cookie)
|
SQL[:delete] = <<-'SQL'
|
||||||
@st_delete ||=
|
|
||||||
@db.prepare(<<-'SQL')
|
|
||||||
DELETE FROM moz_cookies
|
DELETE FROM moz_cookies
|
||||||
WHERE appId = :appId AND
|
WHERE appId = :appId AND
|
||||||
inBrowserElement = :inBrowserElement AND
|
inBrowserElement = :inBrowserElement AND
|
||||||
|
|
@ -228,7 +276,8 @@ class HTTP::CookieJar
|
||||||
path = :path
|
path = :path
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
@st_delete.execute({
|
def db_delete(cookie)
|
||||||
|
@stmt[:delete].execute({
|
||||||
:appId => @app_id,
|
:appId => @app_id,
|
||||||
:inBrowserElement => @in_browser_element ? 1 : 0,
|
:inBrowserElement => @in_browser_element ? 1 : 0,
|
||||||
:name => cookie.name,
|
:name => cookie.name,
|
||||||
|
|
@ -255,11 +304,7 @@ class HTTP::CookieJar
|
||||||
db_delete(cookie)
|
db_delete(cookie)
|
||||||
end
|
end
|
||||||
|
|
||||||
def each(uri = nil, &block)
|
SQL[:cookies_for_domain] = <<-'SQL'
|
||||||
now = Time.now
|
|
||||||
if uri
|
|
||||||
@st_cookies_for_domain ||=
|
|
||||||
@db.prepare(<<-'SQL')
|
|
||||||
SELECT * FROM moz_cookies
|
SELECT * FROM moz_cookies
|
||||||
WHERE baseDomain = :baseDomain AND
|
WHERE baseDomain = :baseDomain AND
|
||||||
appId = :appId AND
|
appId = :appId AND
|
||||||
|
|
@ -267,13 +312,26 @@ class HTTP::CookieJar
|
||||||
expiry >= :expiry
|
expiry >= :expiry
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
@st_update_lastaccessed ||=
|
SQL[:update_lastaccessed] = <<-'SQL'
|
||||||
@db.prepare("UPDATE moz_cookies SET lastAccessed = :lastAccessed where id = :id")
|
UPDATE moz_cookies
|
||||||
|
SET lastAccessed = :lastAccessed
|
||||||
|
WHERE id = :id
|
||||||
|
SQL
|
||||||
|
|
||||||
|
SQL[:all_cookies] = <<-'SQL'
|
||||||
|
SELECT * FROM moz_cookies
|
||||||
|
WHERE appId = :appId AND
|
||||||
|
inBrowserElement = :inBrowserElement AND
|
||||||
|
expiry >= :expiry
|
||||||
|
SQL
|
||||||
|
|
||||||
|
def each(uri = nil, &block)
|
||||||
|
now = Time.now
|
||||||
|
if uri
|
||||||
thost = DomainName.new(uri.host)
|
thost = DomainName.new(uri.host)
|
||||||
tpath = uri.path
|
tpath = uri.path
|
||||||
|
|
||||||
@st_cookies_for_domain.execute({
|
@stmt[:cookies_for_domain].execute({
|
||||||
:baseDomain => thost.domain || thost.hostname,
|
:baseDomain => thost.domain || thost.hostname,
|
||||||
:appId => @app_id,
|
:appId => @app_id,
|
||||||
:inBrowserElement => @in_browser_element ? 1 : 0,
|
:inBrowserElement => @in_browser_element ? 1 : 0,
|
||||||
|
|
@ -297,7 +355,7 @@ class HTTP::CookieJar
|
||||||
|
|
||||||
if cookie.valid_for_uri?(uri)
|
if cookie.valid_for_uri?(uri)
|
||||||
cookie.accessed_at = now
|
cookie.accessed_at = now
|
||||||
@st_update_lastaccessed.execute({
|
@stmt[:update_lastaccessed].execute({
|
||||||
'lastAccessed' => now.to_i,
|
'lastAccessed' => now.to_i,
|
||||||
'id' => row['id'],
|
'id' => row['id'],
|
||||||
})
|
})
|
||||||
|
|
@ -306,15 +364,7 @@ class HTTP::CookieJar
|
||||||
}
|
}
|
||||||
@sjar.each(uri, &block)
|
@sjar.each(uri, &block)
|
||||||
else
|
else
|
||||||
@st_all_cookies ||=
|
@stmt[:all_cookies].execute({
|
||||||
@db.prepare(<<-'SQL')
|
|
||||||
SELECT * FROM moz_cookies
|
|
||||||
WHERE appId = :appId AND
|
|
||||||
inBrowserElement = :inBrowserElement AND
|
|
||||||
expiry >= :expiry
|
|
||||||
SQL
|
|
||||||
|
|
||||||
@st_all_cookies.execute({
|
|
||||||
:appId => @app_id,
|
:appId => @app_id,
|
||||||
:inBrowserElement => @in_browser_element ? 1 : 0,
|
:inBrowserElement => @in_browser_element ? 1 : 0,
|
||||||
:expiry => now.to_i,
|
:expiry => now.to_i,
|
||||||
|
|
@ -344,11 +394,12 @@ class HTTP::CookieJar
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
|
|
||||||
def count
|
SQL[:count] = <<-'SQL'
|
||||||
@st_count ||=
|
SELECT COUNT(id) FROM moz_cookies
|
||||||
@db.prepare("SELECT COUNT(id) FROM moz_cookies")
|
SQL
|
||||||
|
|
||||||
@st_count.execute.first[0]
|
def count
|
||||||
|
@stmt[:count].execute.first[0]
|
||||||
end
|
end
|
||||||
protected :count
|
protected :count
|
||||||
|
|
||||||
|
|
@ -356,44 +407,43 @@ class HTTP::CookieJar
|
||||||
@sjar.empty? && count == 0
|
@sjar.empty? && count == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
def cleanup(session = false)
|
SQL[:delete_expired] = <<-'SQL'
|
||||||
@st_delete_expired ||=
|
DELETE FROM moz_cookies WHERE expiry < :expiry
|
||||||
@db.prepare("DELETE FROM moz_cookies WHERE expiry < :expiry")
|
SQL
|
||||||
|
|
||||||
@st_overusing_domains ||=
|
SQL[:overusing_domains] = <<-'SQL'
|
||||||
@db.prepare(<<-'SQL')
|
|
||||||
SELECT LTRIM(host, '.') domain, COUNT(*) count
|
SELECT LTRIM(host, '.') domain, COUNT(*) count
|
||||||
FROM moz_cookies
|
FROM moz_cookies
|
||||||
GROUP BY domain
|
GROUP BY domain
|
||||||
HAVING count > :count
|
HAVING count > :count
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
@st_delete_per_domain_overuse ||=
|
SQL[:delete_per_domain_overuse] = <<-'SQL'
|
||||||
@db.prepare(<<-'SQL')
|
|
||||||
DELETE FROM moz_cookies WHERE id IN (
|
DELETE FROM moz_cookies WHERE id IN (
|
||||||
SELECT id FROM moz_cookies
|
SELECT id FROM moz_cookies
|
||||||
WHERE LTRIM(host, '.') = :domain
|
WHERE LTRIM(host, '.') = :domain
|
||||||
ORDER BY creationtime
|
ORDER BY creationtime
|
||||||
LIMIT :limit)
|
LIMIT :limit)
|
||||||
SQL
|
SQL
|
||||||
@st_delete_total_overuse ||=
|
|
||||||
@db.prepare(<<-'SQL')
|
SQL[:delete_total_overuse] = <<-'SQL'
|
||||||
DELETE FROM moz_cookies WHERE id IN (
|
DELETE FROM moz_cookies WHERE id IN (
|
||||||
SELECT id FROM moz_cookies ORDER BY creationTime ASC LIMIT :limit
|
SELECT id FROM moz_cookies ORDER BY creationTime ASC LIMIT :limit
|
||||||
)
|
)
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
|
def cleanup(session = false)
|
||||||
synchronize {
|
synchronize {
|
||||||
break if @gc_index == 0
|
break if @gc_index == 0
|
||||||
|
|
||||||
@st_delete_expired.execute({ 'expiry' => Time.now.to_i })
|
@stmt[:delete_expired].execute({ 'expiry' => Time.now.to_i })
|
||||||
|
|
||||||
@st_overusing_domains.execute({
|
@stmt[:overusing_domains].execute({
|
||||||
'count' => HTTP::Cookie::MAX_COOKIES_PER_DOMAIN
|
'count' => HTTP::Cookie::MAX_COOKIES_PER_DOMAIN
|
||||||
}).each { |row|
|
}).each { |row|
|
||||||
domain, count = row['domain'], row['count']
|
domain, count = row['domain'], row['count']
|
||||||
|
|
||||||
@st_delete_per_domain_overuse.execute({
|
@stmt[:delete_per_domain_overuse].execute({
|
||||||
'domain' => domain,
|
'domain' => domain,
|
||||||
'limit' => count - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN,
|
'limit' => count - HTTP::Cookie::MAX_COOKIES_PER_DOMAIN,
|
||||||
})
|
})
|
||||||
|
|
@ -402,7 +452,7 @@ class HTTP::CookieJar
|
||||||
overrun = count - HTTP::Cookie::MAX_COOKIES_TOTAL
|
overrun = count - HTTP::Cookie::MAX_COOKIES_TOTAL
|
||||||
|
|
||||||
if overrun > 0
|
if overrun > 0
|
||||||
@st_delete_total_overuse.execute({ 'limit' => overrun })
|
@stmt[:delete_total_overuse].execute({ 'limit' => overrun })
|
||||||
end
|
end
|
||||||
|
|
||||||
@gc_index = 0
|
@gc_index = 0
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,7 @@ require File.expand_path('helper', File.dirname(__FILE__))
|
||||||
require 'tmpdir'
|
require 'tmpdir'
|
||||||
|
|
||||||
module TestHTTPCookieJar
|
module TestHTTPCookieJar
|
||||||
class TestBasic < Test::Unit::TestCase
|
module CommonTests
|
||||||
def test_store
|
|
||||||
jar = HTTP::CookieJar.new(:store => :hash)
|
|
||||||
assert_instance_of HTTP::CookieJar::HashStore, jar.store
|
|
||||||
|
|
||||||
assert_raises(IndexError) {
|
|
||||||
jar = HTTP::CookieJar.new(:store => :nonexistent)
|
|
||||||
}
|
|
||||||
|
|
||||||
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore.new)
|
|
||||||
assert_instance_of HTTP::CookieJar::HashStore, jar.store
|
|
||||||
|
|
||||||
assert_raises(TypeError) {
|
|
||||||
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module Tests
|
|
||||||
def setup(options = nil, options2 = nil)
|
def setup(options = nil, options2 = nil)
|
||||||
default_options = {
|
default_options = {
|
||||||
:store => :hash,
|
:store => :hash,
|
||||||
|
|
@ -770,11 +752,28 @@ module TestHTTPCookieJar
|
||||||
end
|
end
|
||||||
|
|
||||||
class WithHashStore < Test::Unit::TestCase
|
class WithHashStore < Test::Unit::TestCase
|
||||||
include Tests
|
include CommonTests
|
||||||
|
|
||||||
|
def test_new
|
||||||
|
jar = HTTP::CookieJar.new(:store => :hash)
|
||||||
|
assert_instance_of HTTP::CookieJar::HashStore, jar.store
|
||||||
|
|
||||||
|
assert_raises(IndexError) {
|
||||||
|
jar = HTTP::CookieJar.new(:store => :nonexistent)
|
||||||
|
}
|
||||||
|
|
||||||
|
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore.new)
|
||||||
|
assert_instance_of HTTP::CookieJar::HashStore, jar.store
|
||||||
|
|
||||||
|
assert_raises(TypeError) {
|
||||||
|
jar = HTTP::CookieJar.new(:store => HTTP::CookieJar::HashStore)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
class WithMozillaStore < Test::Unit::TestCase
|
class WithMozillaStore < Test::Unit::TestCase
|
||||||
include Tests
|
include CommonTests
|
||||||
|
|
||||||
def setup
|
def setup
|
||||||
super(
|
super(
|
||||||
|
|
@ -782,6 +781,36 @@ module TestHTTPCookieJar
|
||||||
{ :store => :mozilla, :filename => ":memory:" })
|
{ :store => :mozilla, :filename => ":memory:" })
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def add_and_delete(jar)
|
||||||
|
jar.parse("name=Akinori; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/",
|
||||||
|
'http://rubyforge.org/')
|
||||||
|
jar.parse("country=Japan; Domain=rubyforge.org; Expires=Sun, 08 Aug 2076 19:00:00 GMT; Path=/",
|
||||||
|
'http://rubyforge.org/')
|
||||||
|
jar.delete(HTTP::Cookie.new("name", :domain => 'rubyforge.org'))
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_close
|
||||||
|
add_and_delete(@jar)
|
||||||
|
|
||||||
|
assert_not_send [@jar.store, :closed?]
|
||||||
|
@jar.store.close
|
||||||
|
assert_send [@jar.store, :closed?]
|
||||||
|
@jar.store.close # should do nothing
|
||||||
|
assert_send [@jar.store, :closed?]
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_finalizer
|
||||||
|
db = nil
|
||||||
|
loop {
|
||||||
|
jar = HTTP::CookieJar.new(:store => :mozilla, :filename => ':memory:')
|
||||||
|
add_and_delete(jar)
|
||||||
|
db = jar.store.instance_variable_get(:@db)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
GC.start
|
||||||
|
assert_send [db, :closed?]
|
||||||
|
end
|
||||||
|
|
||||||
def test_upgrade_mozillastore
|
def test_upgrade_mozillastore
|
||||||
Dir.mktmpdir { |dir|
|
Dir.mktmpdir { |dir|
|
||||||
filename = File.join(dir, 'cookies.sqlite')
|
filename = File.join(dir, 'cookies.sqlite')
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue