| /* |
| * 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 |