blob: 0b777acb434798aac180a8500a2eb475e1c656b8 [file] [log] [blame]
/*
* Copyright (C) 2006 Apple Computer, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "IconDatabase.h"
#include "CString.h"
#include "IconDataCache.h"
#include "Image.h"
#include "Logging.h"
#include "SQLStatement.h"
#include "SQLTransaction.h"
#include "SystemTime.h"
#if PLATFORM(WIN_OS)
#include <windows.h>
#include <winbase.h>
#include <shlobj.h>
#else
#include <sys/stat.h>
#endif
namespace WebCore {
static IconDatabase* sharedIconDatabase = 0;
// This version number is in the DB and marks the current generation of the schema
// Theoretically once the switch is flipped this should never change
// Currently, an out-of-date schema causes the DB to be wiped and reset. This isn't
// so bad during development but in the future, we would need to write a conversion
// function to advance older released schemas to "current"
const int currentDatabaseVersion = 5;
// Icons expire once a day
const int iconExpirationTime = 60*60*24;
// Absent icons are rechecked once a week
const int missingIconExpirationTime = 60*60*24*7;
const int updateTimerDelay = 5;
const String& IconDatabase::defaultDatabaseFilename()
{
static String defaultDatabaseFilename = "Icons.db";
return defaultDatabaseFilename;
}
IconDatabase* iconDatabase()
{
if (!sharedIconDatabase)
sharedIconDatabase = new IconDatabase;
return sharedIconDatabase;
}
IconDatabase::IconDatabase()
: m_timeStampForIconURLStatement(0)
, m_iconURLForPageURLStatement(0)
, m_hasIconForIconURLStatement(0)
, m_forgetPageURLStatement(0)
, m_setIconIDForPageURLStatement(0)
, m_getIconIDForIconURLStatement(0)
, m_addIconForIconURLStatement(0)
, m_imageDataForIconURLStatement(0)
, m_importedStatement(0)
, m_setImportedStatement(0)
, m_currentDB(&m_mainDB)
, m_defaultIconDataCache(0)
, m_isEnabled(false)
, m_privateBrowsingEnabled(false)
, m_startupTimer(this, &IconDatabase::pruneUnretainedIconsOnStartup)
, m_updateTimer(this, &IconDatabase::updateDatabase)
, m_initialPruningComplete(false)
, m_imported(false)
, m_isImportedSet(false)
{
}
bool makeAllDirectories(const String& path)
{
#if PLATFORM(WIN_OS)
String fullPath = path;
if (!SHCreateDirectoryEx(0, fullPath.charactersWithNullTermination(), 0)) {
DWORD error = GetLastError();
if (error != ERROR_FILE_EXISTS && error != ERROR_ALREADY_EXISTS) {
LOG_ERROR("Failed to create path %s", path.ascii().data());
return false;
}
}
#else
CString fullPath = path.utf8();
char* p = fullPath.mutableData() + 1;
int length = fullPath.length();
if(p[length - 1] == '/')
p[length - 1] = '\0';
for (; *p; ++p)
if (*p == '/') {
*p = '\0';
if (access(fullPath.data(), F_OK))
if (mkdir(fullPath.data(), S_IRWXU))
return false;
*p = '/';
}
if (access(fullPath.data(), F_OK))
if (mkdir(fullPath.data(), S_IRWXU))
return false;
#endif
return true;
}
bool IconDatabase::open(const String& databasePath)
{
if (!m_isEnabled)
return false;
if (isOpen()) {
LOG_ERROR("Attempt to reopen the IconDatabase which is already open. Must close it first.");
return false;
}
// <rdar://problem/4730811> - Need to create the database path if it doesn't already exist
makeAllDirectories(databasePath);
// First we'll formulate the full path for the database file
String dbFilename;
#if PLATFORM(WIN_OS)
if (databasePath[databasePath.length()] == '\\')
dbFilename = databasePath + defaultDatabaseFilename();
else
dbFilename = databasePath + "\\" + defaultDatabaseFilename();
#else
if (databasePath[databasePath.length()] == '/')
dbFilename = databasePath + defaultDatabaseFilename();
else
dbFilename = databasePath + "/" + defaultDatabaseFilename();
#endif
// <rdar://problem/4707718> - If user's Icon directory is unwritable, Safari will crash at startup
// Now, we'll see if we can open the on-disk database. And, if we can't, we'll return false.
// WebKit will then ignore us and act as if the database is disabled
if (!m_mainDB.open(dbFilename)) {
LOG_ERROR("Unable to open icon database at path %s - %s", dbFilename.ascii().data(), m_mainDB.lastErrorMsg());
return false;
}
if (!isValidDatabase(m_mainDB)) {
LOG(IconDatabase, "%s is missing or in an invalid state - reconstructing", dbFilename.ascii().data());
m_mainDB.clearAllTables();
createDatabaseTables(m_mainDB);
}
// These are actually two different SQLite config options - not my fault they are named confusingly ;)
m_mainDB.setSynchronous(SQLDatabase::SyncOff);
m_mainDB.setFullsync(false);
// Reduce sqlite RAM cache size from default 2000 pages (~1.5kB per page). 3MB of cache for icon database is overkill
if (!SQLStatement(m_mainDB, "PRAGMA cache_size = 200;").executeCommand())
LOG_ERROR("SQLite database could not set cache_size");
// Open the in-memory table for private browsing
if (!m_privateBrowsingDB.open(":memory:"))
LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
// Only if we successfully remained open will we start our "initial purge timer"
// rdar://4690949 - when we have deferred reads and writes all the way in, the prunetimer
// will become "deferredTimer" or something along those lines, and will be set only when
// a deferred read/write is queued
if (isOpen())
m_startupTimer.startOneShot(0);
return isOpen();
}
bool IconDatabase::isOpen() const
{
return m_mainDB.isOpen() && m_privateBrowsingDB.isOpen();
}
void IconDatabase::close()
{
// This will close all the SQL statements and transactions we have open,
// syncing the DB at the appropriate point
deleteAllPreparedStatements(true);
m_mainDB.close();
m_privateBrowsingDB.close();
}
String IconDatabase::databasePath() const
{
return m_mainDB.isOpen() ? m_mainDB.path() : String();
}
void IconDatabase::removeAllIcons()
{
if (!isOpen())
return;
// We don't need to sync anything anymore since we're wiping everything.
// So we can kill the update timer, and clear all the hashes of "items that need syncing"
m_updateTimer.stop();
m_iconDataCachesPendingUpdate.clear();
m_pageURLsPendingAddition.clear();
m_pageURLsPendingDeletion.clear();
m_iconURLsPendingDeletion.clear();
// Now clear all in-memory URLs and Icons
m_pageURLToIconURLMap.clear();
m_pageURLToRetainCount.clear();
m_iconURLToRetainCount.clear();
deleteAllValues(m_iconURLToIconDataCacheMap);
m_iconURLToIconDataCacheMap.clear();
// Wipe any pre-prepared statements, otherwise resetting the SQLDatabases themselves will fail
deleteAllPreparedStatements(false);
// The easiest way to wipe the in-memory database is by closing and reopening it
m_privateBrowsingDB.close();
if (!m_privateBrowsingDB.open(":memory:"))
LOG_ERROR("Unable to open in-memory database for private browsing - %s", m_privateBrowsingDB.lastErrorMsg());
createDatabaseTables(m_privateBrowsingDB);
// To reset the on-disk database, we'll wipe all its tables then vacuum it
// This is easier and safer than closing it, deleting the file, and recreating from scratch
m_mainDB.clearAllTables();
m_mainDB.runVacuumCommand();
createDatabaseTables(m_mainDB);
}
// There are two instances where you'd want to deleteAllPreparedStatements - one with sync, and one without
// A - Closing down the database on application exit - in this case, you *do* want to save the icons out
// B - Resetting the DB via removeAllIcons() - in this case, you *don't* want to sync, because it would be a waste of time
void IconDatabase::deleteAllPreparedStatements(bool withSync)
{
// Sync, if desired
if (withSync)
syncDatabase();
// Order doesn't matter on these
delete m_timeStampForIconURLStatement;
m_timeStampForIconURLStatement = 0;
delete m_iconURLForPageURLStatement;
m_iconURLForPageURLStatement = 0;
delete m_hasIconForIconURLStatement;
m_hasIconForIconURLStatement = 0;
delete m_forgetPageURLStatement;
m_forgetPageURLStatement = 0;
delete m_setIconIDForPageURLStatement;
m_setIconIDForPageURLStatement = 0;
delete m_getIconIDForIconURLStatement;
m_getIconIDForIconURLStatement = 0;
delete m_addIconForIconURLStatement;
m_addIconForIconURLStatement = 0;
delete m_imageDataForIconURLStatement;
m_imageDataForIconURLStatement = 0;
delete m_importedStatement;
m_importedStatement = 0;
delete m_setImportedStatement;
m_setImportedStatement = 0;
}
bool IconDatabase::isEmpty()
{
if (m_privateBrowsingEnabled)
if (!pageURLTableIsEmptyQuery(m_privateBrowsingDB))
return false;
return pageURLTableIsEmptyQuery(m_mainDB);
}
bool IconDatabase::isValidDatabase(SQLDatabase& db)
{
// These two tables should always exist in a valid db
if (!db.tableExists("Icon") || !db.tableExists("PageURL") || !db.tableExists("IconDatabaseInfo"))
return false;
if (SQLStatement(db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0) < currentDatabaseVersion) {
LOG(IconDatabase, "DB version is not found or below expected valid version");
return false;
}
return true;
}
void IconDatabase::createDatabaseTables(SQLDatabase& db)
{
if (!db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
LOG_ERROR("Could not create PageURL table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
db.close();
return;
}
if (!db.executeCommand("CREATE TABLE Icon (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT FAIL, stamp INTEGER, data BLOB);")) {
LOG_ERROR("Could not create Icon table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
db.close();
return;
}
if (!db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", db.lastError(), db.lastErrorMsg());
db.close();
return;
}
if (!db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", db.lastError(), db.lastErrorMsg());
db.close();
return;
}
}
PassRefPtr<SharedBuffer> IconDatabase::imageDataForIconURL(const String& iconURL)
{
// If private browsing is enabled, we'll check there first as the most up-to-date data for an icon will be there
if (m_privateBrowsingEnabled) {
RefPtr<SharedBuffer> result = imageDataForIconURLQuery(m_privateBrowsingDB, iconURL);
if (result && !result->isEmpty())
return result.release();
}
// It wasn't found there, so lets check the main tables
return imageDataForIconURLQuery(m_mainDB, iconURL);
}
void IconDatabase::setPrivateBrowsingEnabled(bool flag)
{
if (!isOpen())
return;
if (m_privateBrowsingEnabled == flag)
return;
// Sync any deferred DB changes before we change the active DB
syncDatabase();
m_privateBrowsingEnabled = flag;
if (m_privateBrowsingEnabled) {
createDatabaseTables(m_privateBrowsingDB);
m_currentDB = &m_privateBrowsingDB;
} else {
m_privateBrowsingDB.clearAllTables();
m_currentDB = &m_mainDB;
}
}
bool IconDatabase::isPrivateBrowsingEnabled() const
{
return m_privateBrowsingEnabled;
}
Image* IconDatabase::iconForPageURL(const String& pageURL, const IntSize& size, bool cache)
{
if (!isOpen())
return defaultIcon(size);
// See if we even have an IconURL for this PageURL...
String iconURL = iconURLForPageURL(pageURL);
if (iconURL.isEmpty())
return 0;
// If we do, maybe we have a IconDataCache for this IconURL
IconDataCache* icon = getOrCreateIconDataCache(iconURL);
// If it's a new IconDataCache object that doesn't have its imageData set yet,
// we'll read in that image data now
if (icon->imageDataStatus() == ImageDataStatusUnknown) {
RefPtr<SharedBuffer> data = imageDataForIconURL(iconURL);
icon->setImageData(data.get());
}
return icon->getImage(size);
}
// FIXME 4667425 - this check needs to see if the icon's data is empty or not and apply
// iconExpirationTime to present icons, and missingIconExpirationTime for missing icons
bool IconDatabase::isIconExpiredForIconURL(const String& iconURL)
{
// If we're closed and someone is making this call, it is likely a return value of
// false will discourage them to take any further action, which is our goal in this case
// Same notion for an empty iconURL - which is now defined as "never expires"
if (!isOpen() || iconURL.isEmpty())
return false;
// If we have a IconDataCache, then it definitely has the Timestamp in it
IconDataCache* icon = m_iconURLToIconDataCacheMap.get(iconURL);
if (icon)
return (int)currentTime() - icon->getTimestamp() > iconExpirationTime;
// Otherwise, we'll get the timestamp from the DB and use it
int stamp;
if (m_privateBrowsingEnabled) {
stamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
if (stamp)
return ((int)currentTime() - stamp) > iconExpirationTime;
}
stamp = timeStampForIconURLQuery(m_mainDB, iconURL);
if (stamp)
return ((int)currentTime() - stamp) > iconExpirationTime;
return false;
}
String IconDatabase::iconURLForPageURL(const String& pageURL)
{
if (!isOpen() || pageURL.isEmpty())
return String();
if (m_pageURLToIconURLMap.contains(pageURL))
return m_pageURLToIconURLMap.get(pageURL);
// Try the private browsing database because if any PageURL's IconURL was updated during privated browsing,
// the most up-to-date iconURL would be there
if (m_privateBrowsingEnabled) {
String iconURL = iconURLForPageURLQuery(m_privateBrowsingDB, pageURL);
if (!iconURL.isEmpty()) {
m_pageURLToIconURLMap.set(pageURL, iconURL);
return iconURL;
}
}
String iconURL = iconURLForPageURLQuery(m_mainDB, pageURL);
if (!iconURL.isEmpty())
m_pageURLToIconURLMap.set(pageURL, iconURL);
return iconURL;
}
Image* IconDatabase::defaultIcon(const IntSize& size)
{
if (!m_defaultIconDataCache) {
m_defaultIconDataCache = new IconDataCache("urlIcon");
m_defaultIconDataCache->loadImageFromResource("urlIcon");
}
return m_defaultIconDataCache->getImage(size);
}
void IconDatabase::retainIconForPageURL(const String& pageURL)
{
if (!isOpen() || pageURL.isEmpty())
return;
// If we don't have the retain count for this page, we need to setup records of its retain
// Otherwise, get the count and increment it
int retainCount;
if (!(retainCount = m_pageURLToRetainCount.get(pageURL))) {
m_pageURLToRetainCount.set(pageURL, 1);
// If we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot,
// so we bail here and skip those steps
if (!m_initialPruningComplete)
return;
// If this pageURL is marked for deletion, bring it back from the brink
m_pageURLsPendingDeletion.remove(pageURL);
// If we have an iconURL for this pageURL, we'll now retain the iconURL
String iconURL = iconURLForPageURL(pageURL);
if (!iconURL.isEmpty())
retainIconURL(iconURL);
} else
m_pageURLToRetainCount.set(pageURL, retainCount + 1);
}
void IconDatabase::releaseIconForPageURL(const String& pageURL)
{
if (!isOpen() || pageURL.isEmpty())
return;
// Check if this pageURL is actually retained
if (!m_pageURLToRetainCount.contains(pageURL)) {
LOG_ERROR("Attempting to release icon for URL %s which is not retained", pageURL.ascii().data());
return;
}
// Get its retain count
int retainCount = m_pageURLToRetainCount.get(pageURL);
ASSERT(retainCount > 0);
// If it still has a positive retain count, store the new count and bail
if (--retainCount) {
m_pageURLToRetainCount.set(pageURL, retainCount);
return;
}
LOG(IconDatabase, "No more retainers for PageURL %s", pageURL.ascii().data());
// Otherwise, remove all record of the retain count
m_pageURLToRetainCount.remove(pageURL);
// If we haven't done pruning yet, we want to avoid any pageURL->iconURL lookups and the pageURLsPendingDeletion is moot,
// so we bail here and skip those steps
if (!m_initialPruningComplete)
return;
// Then mark this pageURL for deletion
m_pageURLsPendingDeletion.add(pageURL);
// Grab the iconURL and release it
String iconURL = iconURLForPageURL(pageURL);
if (!iconURL.isEmpty())
releaseIconURL(iconURL);
}
void IconDatabase::retainIconURL(const String& iconURL)
{
ASSERT(!iconURL.isEmpty());
if (int retainCount = m_iconURLToRetainCount.get(iconURL)) {
ASSERT(retainCount > 0);
m_iconURLToRetainCount.set(iconURL, retainCount + 1);
} else {
m_iconURLToRetainCount.set(iconURL, 1);
if (m_iconURLsPendingDeletion.contains(iconURL))
m_iconURLsPendingDeletion.remove(iconURL);
}
}
void IconDatabase::releaseIconURL(const String& iconURL)
{
ASSERT(!iconURL.isEmpty());
// If the iconURL has no retain count, we can bail
if (!m_iconURLToRetainCount.contains(iconURL))
return;
// Otherwise, decrement it
int retainCount = m_iconURLToRetainCount.get(iconURL) - 1;
ASSERT(retainCount > -1);
// If the icon is still retained, store the count and bail
if (retainCount) {
m_iconURLToRetainCount.set(iconURL, retainCount);
return;
}
LOG(IconDatabase, "No more retainers for IconURL %s", iconURL.ascii().data());
// Otherwise, this icon is toast. Remove all traces of its retain count...
m_iconURLToRetainCount.remove(iconURL);
// And since we delay the actual deletion of icons, so lets add it to that queue
m_iconURLsPendingDeletion.add(iconURL);
}
void IconDatabase::forgetPageURL(const String& pageURL)
{
// Remove the PageURL->IconURL mapping
m_pageURLToIconURLMap.remove(pageURL);
// And remove this pageURL from the DB
forgetPageURLQuery(*m_currentDB, pageURL);
}
bool IconDatabase::isIconURLRetained(const String& iconURL)
{
if (iconURL.isEmpty())
return false;
return m_iconURLToRetainCount.contains(iconURL);
}
void IconDatabase::forgetIconForIconURLFromDatabase(const String& iconURL)
{
if (iconURL.isEmpty())
return;
// For private browsing safety, since this alters the database, we only forget from the current database
// If we're in private browsing and the Icon also exists in the main database, it will be pruned on the next startup
int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL, false);
// If we didn't actually have an icon for this iconURL... well, thats a screwy condition we should track down, but also
// something we could move on from
ASSERT(iconID);
if (!iconID) {
LOG_ERROR("Attempting to forget icon for IconURL %s, though we don't have it in the database", iconURL.ascii().data());
return;
}
if (!m_currentDB->executeCommand(String::format("DELETE FROM Icon WHERE Icon.iconID = %lli;", iconID)))
LOG_ERROR("Unable to drop Icon for IconURL", iconURL.ascii().data());
if (!m_currentDB->executeCommand(String::format("DELETE FROM PageURL WHERE PageURL.iconID = %lli", iconID)))
LOG_ERROR("Unable to drop all PageURL for IconURL", iconURL.ascii().data());
}
IconDataCache* IconDatabase::getOrCreateIconDataCache(const String& iconURL)
{
IconDataCache* icon;
if ((icon = m_iconURLToIconDataCacheMap.get(iconURL)))
return icon;
icon = new IconDataCache(iconURL);
m_iconURLToIconDataCacheMap.set(iconURL, icon);
// Get the most current time stamp for this IconURL
int timestamp = 0;
if (m_privateBrowsingEnabled)
timestamp = timeStampForIconURLQuery(m_privateBrowsingDB, iconURL);
if (!timestamp)
timestamp = timeStampForIconURLQuery(m_mainDB, iconURL);
// If we can't get a timestamp for this URL, then it is a new icon and we initialize its timestamp now
if (!timestamp) {
icon->setTimestamp((int)currentTime());
m_iconDataCachesPendingUpdate.add(icon);
} else
icon->setTimestamp(timestamp);
return icon;
}
void IconDatabase::setIconDataForIconURL(PassRefPtr<SharedBuffer> data, const String& iconURL)
{
if (!isOpen() || iconURL.isEmpty())
return;
// Get the IconDataCache for this IconURL (note, IconDataCacheForIconURL will create it if necessary)
IconDataCache* icon = getOrCreateIconDataCache(iconURL);
// Set the data in the IconDataCache
icon->setImageData(data);
// Update the timestamp in the IconDataCache to NOW
icon->setTimestamp((int)currentTime());
// Mark the IconDataCache as requiring an update to the database
m_iconDataCachesPendingUpdate.add(icon);
}
void IconDatabase::setHaveNoIconForIconURL(const String& iconURL)
{
setIconDataForIconURL(0, iconURL);
}
bool IconDatabase::setIconURLForPageURL(const String& iconURL, const String& pageURL)
{
ASSERT(!iconURL.isEmpty());
if (!isOpen() || pageURL.isEmpty())
return false;
// If the urls already map to each other, bail.
// This happens surprisingly often, and seems to cream iBench performance
if (m_pageURLToIconURLMap.get(pageURL) == iconURL)
return false;
// If this pageURL is retained, we have some work to do on the IconURL retain counts
if (m_pageURLToRetainCount.contains(pageURL)) {
String oldIconURL = m_pageURLToIconURLMap.get(pageURL);
if (!oldIconURL.isEmpty())
releaseIconURL(oldIconURL);
retainIconURL(iconURL);
} else {
// If this pageURL is *not* retained, then we may be marking it for deletion, as well!
// As counterintuitive as it seems to mark it for addition and for deletion at the same time,
// it's valid because when we do a new pageURL->iconURL mapping we *have* to mark it for addition,
// no matter what, as there is no efficient was to determine if the mapping is in the DB already.
// But, if the iconURL is marked for deletion, we'll also mark this pageURL for deletion - if a
// client comes along and retains it before the timer fires, the "pendingDeletion" lists will
// be manipulated appopriately and new pageURL will be brought back from the brink
if (m_iconURLsPendingDeletion.contains(iconURL))
m_pageURLsPendingDeletion.add(pageURL);
}
// Cache the pageURL->iconURL map
m_pageURLToIconURLMap.set(pageURL, iconURL);
// And mark this mapping to be added to the database
m_pageURLsPendingAddition.add(pageURL);
// Then start the timer to commit this change - or further delay the timer if it
// was already started
m_updateTimer.startOneShot(updateTimerDelay);
return true;
}
void IconDatabase::setIconURLForPageURLInDatabase(const String& iconURL, const String& pageURL)
{
int64_t iconID = establishIconIDForIconURL(*m_currentDB, iconURL);
if (!iconID) {
LOG_ERROR("Failed to establish an ID for iconURL %s", iconURL.ascii().data());
return;
}
setIconIDForPageURLQuery(*m_currentDB, iconID, pageURL);
}
int64_t IconDatabase::establishIconIDForIconURL(SQLDatabase& db, const String& iconURL, bool createIfNecessary)
{
// Get the iconID thats already in this database and return it - or return 0 if we're read-only
int64_t iconID = getIconIDForIconURLQuery(db, iconURL);
if (iconID || !createIfNecessary)
return iconID;
// Create the icon table entry for the iconURL
return addIconForIconURLQuery(db, iconURL);
}
void IconDatabase::pruneUnretainedIconsOnStartup(Timer<IconDatabase>*)
{
if (!isOpen())
return;
// This function should only be called once per run, and ideally only via the timer
// on program startup
ASSERT(!m_initialPruningComplete);
#ifndef NDEBUG
double timestamp = currentTime();
#endif
// rdar://4690949 - Need to prune unretained iconURLs here, then prune out all pageURLs that reference
// nonexistent icons
SQLTransaction pruningTransaction(m_mainDB);
pruningTransaction.begin();
// Wipe all PageURLs that aren't retained
// Temporary tables in sqlite seem to lose memory permanently so do this by hand instead. This is faster too.
Vector<int64_t> pageURLIconIDsToDelete;
// Get the known PageURLs from the db, and record the ID of any that are not in the retain count set.
SQLStatement pageSQL(m_mainDB, "SELECT url, iconID FROM PageURL");
pageSQL.prepare();
int result;
while ((result = pageSQL.step()) == SQLResultRow) {
String pageURL = pageSQL.getColumnText16(0);
if (pageURL.isEmpty() || !m_pageURLToRetainCount.contains(pageURL))
pageURLIconIDsToDelete.append(pageSQL.getColumnInt64(1));
}
pageSQL.finalize();
if (result != SQLResultDone)
LOG_ERROR("Error reading PageURL table from on-disk DB");
// Delete page URLs that were in the table, but not in our retain count set.
size_t numToDelete = pageURLIconIDsToDelete.size();
if (numToDelete) {
SQLStatement pageDeleteSQL(m_mainDB, "DELETE FROM PageURL WHERE iconID = (?)");
pageDeleteSQL.prepare();
for (size_t i = 0; i < numToDelete; ++i) {
pageDeleteSQL.bindInt64(1, pageURLIconIDsToDelete[i]);
if (pageDeleteSQL.step() != SQLResultDone)
LOG_ERROR("Unable to delete icon ID %llu from PageURL table", static_cast<unsigned long long>(pageURLIconIDsToDelete[i]));
pageDeleteSQL.reset();
}
pageDeleteSQL.finalize();
}
// Wipe Icons that aren't retained
if (!m_mainDB.executeCommand("DELETE FROM Icon WHERE Icon.iconID NOT IN (SELECT iconID FROM PageURL);"))
LOG_ERROR("Failed to execute SQL to prune unretained icons from the on-disk tables");
// Since we lazily retained the pageURLs without getting the iconURLs or retaining the iconURLs,
// we need to do that now
SQLStatement sql(m_mainDB, "SELECT PageURL.url, Icon.url FROM PageURL INNER JOIN Icon ON PageURL.iconID=Icon.iconID");
sql.prepare();
while((result = sql.step()) == SQLResultRow) {
String iconURL = sql.getColumnText16(1);
retainIconURL(iconURL);
LOG(IconDatabase, "Found a PageURL that mapped to %s", iconURL.ascii().data());
}
if (result != SQLResultDone)
LOG_ERROR("Error reading PageURL->IconURL mappings from on-disk DB");
sql.finalize();
pruningTransaction.commit();
m_initialPruningComplete = true;
// Handle dangling PageURLs, if any
checkForDanglingPageURLs(true);
#ifndef NDEBUG
timestamp = currentTime() - timestamp;
if (timestamp <= 1.0)
LOG(IconDatabase, "Pruning unretained icons took %.4f seconds", timestamp);
else
LOG(IconDatabase, "Pruning unretained icons took %.4f seconds - this is much too long!", timestamp);
#endif
}
void IconDatabase::updateDatabase(Timer<IconDatabase>*)
{
syncDatabase();
}
void IconDatabase::syncDatabase()
{
#ifndef NDEBUG
double timestamp = currentTime();
#endif
// First we'll do the pending additions
// Starting with the IconDataCaches that need updating/insertion
for (HashSet<IconDataCache*>::iterator i = m_iconDataCachesPendingUpdate.begin(), end = m_iconDataCachesPendingUpdate.end(); i != end; ++i) {
(*i)->writeToDatabase(*m_currentDB);
LOG(IconDatabase, "Wrote IconDataCache for IconURL %s with timestamp of %li to the DB", (*i)->getIconURL().ascii().data(), (*i)->getTimestamp());
}
m_iconDataCachesPendingUpdate.clear();
HashSet<String>::iterator i = m_pageURLsPendingAddition.begin(), end = m_pageURLsPendingAddition.end();
for (; i != end; ++i) {
setIconURLForPageURLInDatabase(m_pageURLToIconURLMap.get(*i), *i);
LOG(IconDatabase, "Committed IconURL for PageURL %s to database", (*i).ascii().data());
}
m_pageURLsPendingAddition.clear();
// Then we'll do the pending deletions
// First lets wipe all the pageURLs
for (i = m_pageURLsPendingDeletion.begin(), end = m_pageURLsPendingDeletion.end(); i != end; ++i) {
forgetPageURL(*i);
LOG(IconDatabase, "Deleted PageURL %s", (*i).ascii().data());
}
m_pageURLsPendingDeletion.clear();
// Then get rid of all traces of the icons and IconURLs
IconDataCache* icon;
for (i = m_iconURLsPendingDeletion.begin(), end = m_iconURLsPendingDeletion.end(); i != end; ++i) {
// Forget the IconDataCache
icon = m_iconURLToIconDataCacheMap.get(*i);
if (icon)
m_iconURLToIconDataCacheMap.remove(*i);
delete icon;
// Forget the IconURL from the database
forgetIconForIconURLFromDatabase(*i);
LOG(IconDatabase, "Deleted icon %s", (*i).ascii().data());
}
m_iconURLsPendingDeletion.clear();
// If the timer was running to cause this update, we can kill the timer as its firing would be redundant
m_updateTimer.stop();
#ifndef NDEBUG
timestamp = currentTime() - timestamp;
if (timestamp <= 1.0)
LOG(IconDatabase, "Updating the database took %.4f seconds", timestamp);
else
LOG(IconDatabase, "Updating the database took %.4f seconds - this is much too long!", timestamp);
// Check to make sure there are no dangling PageURLs - If there are, we want to output one log message but not spam the console potentially every few seconds
checkForDanglingPageURLs(false);
#endif
}
void IconDatabase::checkForDanglingPageURLs(bool pruneIfFound)
{
// We don't want to keep performing this check and reporting this error if it has already found danglers so we keep track
static bool danglersFound = false;
// However, if the caller wants us to prune the danglers, we will reset this flag and prune every time
if (pruneIfFound)
danglersFound = false;
if (!danglersFound && SQLStatement(*m_currentDB, "SELECT url FROM PageURL WHERE PageURL.iconID NOT IN (SELECT iconID FROM Icon) LIMIT 1;").returnsAtLeastOneResult()) {
danglersFound = true;
LOG_ERROR("Dangling PageURL entries found");
if (pruneIfFound && !m_currentDB->executeCommand("DELETE FROM PageURL WHERE iconID NOT IN (SELECT iconID FROM Icon);"))
LOG_ERROR("Unable to prune dangling PageURLs");
}
}
bool IconDatabase::hasEntryForIconURL(const String& iconURL)
{
if (!isOpen() || iconURL.isEmpty())
return false;
// First check the in memory mapped icons...
if (m_iconURLToIconDataCacheMap.contains(iconURL))
return true;
// Then we'll check the main database
if (hasIconForIconURLQuery(m_mainDB, iconURL))
return true;
// Finally, the last resort - check the private browsing database
if (m_privateBrowsingEnabled)
if (hasIconForIconURLQuery(m_privateBrowsingDB, iconURL))
return true;
// We must not have this iconURL!
return false;
}
void IconDatabase::setEnabled(bool enabled)
{
if (!enabled && isOpen())
close();
m_isEnabled = enabled;
}
bool IconDatabase::enabled() const
{
return m_isEnabled;
}
bool IconDatabase::imported()
{
if (!m_isImportedSet) {
m_imported = importedQuery(m_mainDB);
m_isImportedSet = true;
}
return m_imported;
}
void IconDatabase::setImported(bool import)
{
m_imported = import;
m_isImportedSet = true;
setImportedQuery(m_mainDB, import);
}
IconDatabase::~IconDatabase()
{
ASSERT_NOT_REACHED();
}
// readySQLStatement() handles two things
// 1 - If the SQLDatabase& argument is different, the statement must be destroyed and remade. This happens when the user
// switches to and from private browsing
// 2 - Lazy construction of the Statement in the first place, in case we've never made this query before
inline void readySQLStatement(SQLStatement*& statement, SQLDatabase& db, const String& str)
{
if (statement && (statement->database() != &db || statement->isExpired())) {
if (statement->isExpired())
LOG(IconDatabase, "SQLStatement associated with %s is expired", str.ascii().data());
delete statement;
statement = 0;
}
if (!statement) {
statement = new SQLStatement(db, str);
int result;
result = statement->prepare();
ASSERT(result == SQLResultOk);
}
}
// Any common IconDatabase query should be seperated into a fooQuery() and a *m_fooStatement.
// This way we can lazily construct the SQLStatment for a query on its first use, then reuse the Statement binding
// the new parameter as needed
// The statement must be deleted in IconDatabase::close() before the actual SQLDatabase::close() call
// Also, m_fooStatement must be reset() before fooQuery() returns otherwise we will constantly get "database file locked"
// errors in various combinations of queries
bool IconDatabase::pageURLTableIsEmptyQuery(SQLDatabase& db)
{
// We won't make this use a m_fooStatement because its not really a "common" query
return !SQLStatement(db, "SELECT iconID FROM PageURL LIMIT 1;").returnsAtLeastOneResult();
}
PassRefPtr<SharedBuffer> IconDatabase::imageDataForIconURLQuery(SQLDatabase& db, const String& iconURL)
{
RefPtr<SharedBuffer> imageData;
readySQLStatement(m_imageDataForIconURLStatement, db, "SELECT Icon.data FROM Icon WHERE Icon.url = (?);");
m_imageDataForIconURLStatement->bindText16(1, iconURL, false);
int result = m_imageDataForIconURLStatement->step();
if (result == SQLResultRow) {
Vector<char> data;
m_imageDataForIconURLStatement->getColumnBlobAsVector(0, data);
imageData = new SharedBuffer;
imageData->append(data.data(), data.size());
} else if (result != SQLResultDone)
LOG_ERROR("imageDataForIconURLQuery failed");
m_imageDataForIconURLStatement->reset();
return imageData.release();
}
int IconDatabase::timeStampForIconURLQuery(SQLDatabase& db, const String& iconURL)
{
readySQLStatement(m_timeStampForIconURLStatement, db, "SELECT Icon.stamp FROM Icon WHERE Icon.url = (?);");
m_timeStampForIconURLStatement->bindText16(1, iconURL, false);
int result = m_timeStampForIconURLStatement->step();
if (result == SQLResultRow)
result = m_timeStampForIconURLStatement->getColumnInt(0);
else {
if (result != SQLResultDone)
LOG_ERROR("timeStampForIconURLQuery failed");
result = 0;
}
m_timeStampForIconURLStatement->reset();
return result;
}
String IconDatabase::iconURLForPageURLQuery(SQLDatabase& db, const String& pageURL)
{
readySQLStatement(m_iconURLForPageURLStatement, db, "SELECT Icon.url FROM Icon, PageURL WHERE PageURL.url = (?) AND Icon.iconID = PageURL.iconID;");
m_iconURLForPageURLStatement->bindText16(1, pageURL, false);
int result = m_iconURLForPageURLStatement->step();
String iconURL;
if (result == SQLResultRow)
iconURL = m_iconURLForPageURLStatement->getColumnText16(0);
else if (result != SQLResultDone)
LOG_ERROR("iconURLForPageURLQuery failed");
m_iconURLForPageURLStatement->reset();
return iconURL;
}
void IconDatabase::forgetPageURLQuery(SQLDatabase& db, const String& pageURL)
{
readySQLStatement(m_forgetPageURLStatement, db, "DELETE FROM PageURL WHERE url = (?);");
m_forgetPageURLStatement->bindText16(1, pageURL, false);
if (m_forgetPageURLStatement->step() != SQLResultDone)
LOG_ERROR("forgetPageURLQuery failed");
m_forgetPageURLStatement->reset();
}
void IconDatabase::setIconIDForPageURLQuery(SQLDatabase& db, int64_t iconID, const String& pageURL)
{
readySQLStatement(m_setIconIDForPageURLStatement, db, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
m_setIconIDForPageURLStatement->bindText16(1, pageURL, false);
m_setIconIDForPageURLStatement->bindInt64(2, iconID);
if (m_setIconIDForPageURLStatement->step() != SQLResultDone)
LOG_ERROR("setIconIDForPageURLQuery failed");
m_setIconIDForPageURLStatement->reset();
}
int64_t IconDatabase::getIconIDForIconURLQuery(SQLDatabase& db, const String& iconURL)
{
readySQLStatement(m_getIconIDForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
m_getIconIDForIconURLStatement->bindText16(1, iconURL, false);
int64_t result = m_getIconIDForIconURLStatement->step();
if (result == SQLResultRow)
result = m_getIconIDForIconURLStatement->getColumnInt64(0);
else {
if (result != SQLResultDone)
LOG_ERROR("getIconIDForIconURLQuery failed");
result = 0;
}
m_getIconIDForIconURLStatement->reset();
return result;
}
int64_t IconDatabase::addIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
{
readySQLStatement(m_addIconForIconURLStatement, db, "INSERT INTO Icon (url) VALUES ((?));");
m_addIconForIconURLStatement->bindText16(1, iconURL, false);
int64_t result = m_addIconForIconURLStatement->step();
if (result == SQLResultDone)
result = db.lastInsertRowID();
else {
LOG_ERROR("addIconForIconURLQuery failed");
result = 0;
}
m_addIconForIconURLStatement->reset();
return result;
}
bool IconDatabase::hasIconForIconURLQuery(SQLDatabase& db, const String& iconURL)
{
readySQLStatement(m_hasIconForIconURLStatement, db, "SELECT Icon.iconID FROM Icon WHERE Icon.url = (?);");
m_hasIconForIconURLStatement->bindText16(1, iconURL, false);
int result = m_hasIconForIconURLStatement->step();
if (result != SQLResultRow && result != SQLResultDone)
LOG_ERROR("hasIconForIconURLQuery failed");
m_hasIconForIconURLStatement->reset();
return result == SQLResultRow;
}
bool IconDatabase::importedQuery(SQLDatabase& db)
{
readySQLStatement(m_importedStatement, db, "SELECT IconDatabaseInfo.value FROM IconDatabaseInfo WHERE IconDatabaseInfo.key = \"ImportedSafari2Icons\";");
int result = m_importedStatement->step();
if (result == SQLResultRow)
result = m_importedStatement->getColumnInt(0);
else {
if (result != SQLResultDone)
LOG_ERROR("importedQuery failed");
result = 0;
}
m_importedStatement->reset();
return result;
}
void IconDatabase::setImportedQuery(SQLDatabase& db, bool imported)
{
if (imported)
readySQLStatement(m_setImportedStatement, db, "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 1);");
else
readySQLStatement(m_setImportedStatement, db, "INSERT INTO IconDatabaseInfo (key, value) VALUES (\"ImportedSafari2Icons\", 0);");
int result = m_setImportedStatement->step();
if (result != SQLResultDone)
LOG_ERROR("setImportedQuery failed");
m_setImportedStatement->reset();
}
} // namespace WebCore