| /* |
| * Copyright (C) 2007, 2008, 2012, 2013 Apple 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. |
| * 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| * its contributors may be used to endorse or promote products derived |
| * from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "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 OR ITS 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 "DatabaseTracker.h" |
| |
| #include "Database.h" |
| #include "DatabaseContext.h" |
| #include "DatabaseManager.h" |
| #include "DatabaseManagerClient.h" |
| #include "DatabaseThread.h" |
| #include "Logging.h" |
| #include "OriginLock.h" |
| #include "SecurityOrigin.h" |
| #include "SecurityOriginData.h" |
| #include "SecurityOriginHash.h" |
| #include "SQLiteFileSystem.h" |
| #include "SQLiteStatement.h" |
| #include "SQLiteTransaction.h" |
| #include <wtf/FileSystem.h> |
| #include <wtf/MainThread.h> |
| #include <wtf/NeverDestroyed.h> |
| #include <wtf/StdLibExtras.h> |
| #include <wtf/UUID.h> |
| #include <wtf/text/CString.h> |
| #include <wtf/text/StringBuilder.h> |
| |
| #if PLATFORM(IOS_FAMILY) |
| #include "WebCoreThread.h" |
| #endif |
| |
| namespace WebCore { |
| |
| static Vector<String> isolatedCopy(const Vector<String>& original) |
| { |
| Vector<String> copy; |
| copy.reserveInitialCapacity(original.size()); |
| for (auto& string : original) |
| copy.uncheckedAppend(string.isolatedCopy()); |
| return copy; |
| } |
| |
| std::unique_ptr<DatabaseTracker> DatabaseTracker::trackerWithDatabasePath(const String& databasePath) |
| { |
| return std::unique_ptr<DatabaseTracker>(new DatabaseTracker(databasePath)); |
| } |
| |
| static DatabaseTracker* staticTracker = nullptr; |
| |
| void DatabaseTracker::initializeTracker(const String& databasePath) |
| { |
| ASSERT(!staticTracker); |
| if (staticTracker) |
| return; |
| staticTracker = new DatabaseTracker(databasePath); |
| } |
| |
| bool DatabaseTracker::isInitialized() |
| { |
| return !!staticTracker; |
| } |
| |
| DatabaseTracker& DatabaseTracker::singleton() |
| { |
| if (!staticTracker) |
| staticTracker = new DatabaseTracker(emptyString()); |
| return *staticTracker; |
| } |
| |
| DatabaseTracker::DatabaseTracker(const String& databasePath) |
| : m_databaseDirectoryPath(databasePath.isolatedCopy()) |
| { |
| } |
| |
| String DatabaseTracker::trackerDatabasePath() const |
| { |
| return SQLiteFileSystem::appendDatabaseFileNameToPath(m_databaseDirectoryPath.isolatedCopy(), "Databases.db"); |
| } |
| |
| void DatabaseTracker::openTrackerDatabase(TrackerCreationAction createAction) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| if (m_database.isOpen()) |
| return; |
| |
| // If createIfDoesNotExist is false, SQLiteFileSystem::ensureDatabaseFileExists() |
| // will return false if the database file does not exist. |
| // If createIfDoesNotExist is true, SQLiteFileSystem::ensureDatabaseFileExists() |
| // will attempt to create the path to the database file if it does not |
| // exists yet. It'll return true if the path already exists, or if it |
| // successfully creates the path. Else, it will return false. |
| String databasePath = trackerDatabasePath(); |
| if (!SQLiteFileSystem::ensureDatabaseFileExists(databasePath, createAction == CreateIfDoesNotExist)) |
| return; |
| |
| if (!m_database.open(databasePath)) { |
| // FIXME: What do do here? |
| LOG_ERROR("Failed to open databasePath %s.", databasePath.utf8().data()); |
| return; |
| } |
| m_database.disableThreadingChecks(); |
| |
| if (!m_database.tableExists("Origins")) { |
| if (!m_database.executeCommand("CREATE TABLE Origins (origin TEXT UNIQUE ON CONFLICT REPLACE, quota INTEGER NOT NULL ON CONFLICT FAIL);"_s)) { |
| // FIXME: and here |
| LOG_ERROR("Failed to create Origins table"); |
| } |
| } |
| |
| if (!m_database.tableExists("Databases")) { |
| if (!m_database.executeCommand("CREATE TABLE Databases (guid INTEGER PRIMARY KEY AUTOINCREMENT, origin TEXT, name TEXT, displayName TEXT, estimatedSize INTEGER, path TEXT);"_s)) { |
| // FIXME: and here |
| LOG_ERROR("Failed to create Databases table"); |
| } |
| } |
| } |
| |
| ExceptionOr<void> DatabaseTracker::hasAdequateQuotaForOrigin(const SecurityOriginData& origin, uint64_t estimatedSize) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| auto usage = this->usage(origin); |
| |
| // If the database will fit, allow its creation. |
| auto requirement = usage + std::max<uint64_t>(1u, estimatedSize); |
| if (requirement < usage) { |
| // The estimated size is so big it causes an overflow; don't allow creation. |
| return Exception { SecurityError }; |
| } |
| if (requirement > quotaNoLock(origin)) |
| return Exception { QuotaExceededError }; |
| return { }; |
| } |
| |
| ExceptionOr<void> DatabaseTracker::canEstablishDatabase(DatabaseContext& context, const String& name, uint64_t estimatedSize) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| |
| // FIXME: What guarantees this context.securityOrigin() is non-null? |
| auto origin = context.securityOrigin(); |
| |
| if (isDeletingDatabaseOrOriginFor(origin, name)) |
| return Exception { SecurityError }; |
| |
| recordCreatingDatabase(origin, name); |
| |
| // If a database already exists, ignore the passed-in estimated size and say it's OK. |
| if (hasEntryForDatabase(origin, name)) |
| return { }; |
| |
| auto result = hasAdequateQuotaForOrigin(origin, estimatedSize); |
| if (!result.hasException()) |
| return { }; |
| |
| // If we get here, then we do not have enough quota for one of the |
| // following reasons as indicated by the set error: |
| // |
| // If the error is DatabaseSizeOverflowed, then this means the requested |
| // estimatedSize if so unreasonably large that it can cause an overflow in |
| // the usage budget computation. In that case, there's nothing more we can |
| // do, and there's no need for a retry. Hence, we should indicate that |
| // we're done with our attempt to create the database. |
| // |
| // If the error is DatabaseSizeExceededQuota, then we'll give the client |
| // a chance to update the quota and call retryCanEstablishDatabase() to try |
| // again. Hence, we don't call doneCreatingDatabase() yet in that case. |
| |
| auto exception = result.releaseException(); |
| if (exception.code() != QuotaExceededError) |
| doneCreatingDatabase(origin, name); |
| |
| return exception; |
| } |
| |
| // Note: a thought about performance: hasAdequateQuotaForOrigin() was also |
| // called in canEstablishDatabase(), and hence, we're repeating some work within |
| // hasAdequateQuotaForOrigin(). However, retryCanEstablishDatabase() should only |
| // be called in the rare even if canEstablishDatabase() fails. Since it is rare, |
| // we should not bother optimizing it. It is more beneficial to keep |
| // hasAdequateQuotaForOrigin() simple and correct (i.e. bug free), and just |
| // re-use it. Also note that the path for opening a database involves IO, and |
| // hence should not be a performance critical path anyway. |
| ExceptionOr<void> DatabaseTracker::retryCanEstablishDatabase(DatabaseContext& context, const String& name, uint64_t estimatedSize) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| |
| // FIXME: What guarantees context.securityOrigin() is non-null? |
| auto origin = context.securityOrigin(); |
| |
| // We have already eliminated other types of errors in canEstablishDatabase(). |
| // The only reason we're in retryCanEstablishDatabase() is because we gave |
| // the client a chance to update the quota and are rechecking it here. |
| // If we fail this check, the only possible reason this time should be due |
| // to inadequate quota. |
| auto result = hasAdequateQuotaForOrigin(origin, estimatedSize); |
| if (!result.hasException()) |
| return { }; |
| |
| auto exception = result.releaseException(); |
| ASSERT(exception.code() == QuotaExceededError); |
| doneCreatingDatabase(origin, name); |
| |
| return exception; |
| } |
| |
| bool DatabaseTracker::hasEntryForOriginNoLock(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return false; |
| |
| auto statement = m_database.prepareStatement("SELECT origin FROM Origins where origin=?;"_s); |
| if (!statement) { |
| LOG_ERROR("Failed to prepare statement."); |
| return false; |
| } |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| |
| return statement->step() == SQLITE_ROW; |
| } |
| |
| bool DatabaseTracker::hasEntryForDatabase(const SecurityOriginData& origin, const String& databaseIdentifier) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) { |
| // No "tracker database". Hence, no entry for the database of interest. |
| return false; |
| } |
| |
| // We've got a tracker database. Set up a query to ask for the db of interest: |
| auto statement = m_database.prepareStatement("SELECT guid FROM Databases WHERE origin=? AND name=?;"_s); |
| |
| if (!statement) |
| return false; |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| statement->bindText(2, databaseIdentifier); |
| |
| return statement->step() == SQLITE_ROW; |
| } |
| |
| uint64_t DatabaseTracker::maximumSize(Database& database) |
| { |
| // The maximum size for a database is the full quota for its origin, minus the current usage within the origin, |
| // plus the current usage of the given database |
| Locker lockDatabase { m_databaseGuard }; |
| auto origin = database.securityOrigin(); |
| |
| auto quota = quotaNoLock(origin); |
| auto diskUsage = usage(origin); |
| auto databaseFileSize = SQLiteFileSystem::databaseFileSize(database.fileNameIsolatedCopy()); |
| ASSERT(databaseFileSize <= diskUsage); |
| |
| if (diskUsage > quota) |
| return databaseFileSize; |
| |
| // A previous error may have allowed the origin to exceed its quota, or may |
| // have allowed this database to exceed our cached estimate of the origin |
| // disk usage. Don't multiply that error through integer underflow, or the |
| // effective quota will permanently become 2^64. |
| uint64_t maxSize = quota - diskUsage + databaseFileSize; |
| if (maxSize > quota) |
| maxSize = databaseFileSize; |
| return maxSize; |
| } |
| |
| void DatabaseTracker::closeAllDatabases(CurrentQueryBehavior currentQueryBehavior) |
| { |
| for (auto& database : openDatabases()) { |
| if (currentQueryBehavior == CurrentQueryBehavior::Interrupt) |
| database->interrupt(); |
| database->close(); |
| } |
| } |
| |
| String DatabaseTracker::originPath(const SecurityOriginData& origin) const |
| { |
| return SQLiteFileSystem::appendDatabaseFileNameToPath(m_databaseDirectoryPath.isolatedCopy(), origin.databaseIdentifier()); |
| } |
| |
| static String generateDatabaseFileName() |
| { |
| return makeString(createCanonicalUUIDString(), ".db"); |
| } |
| |
| String DatabaseTracker::fullPathForDatabaseNoLock(const SecurityOriginData& origin, const String& name, bool createIfNotExists) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| String originIdentifier = origin.databaseIdentifier(); |
| String originPath = this->originPath(origin); |
| |
| // Make sure the path for this SecurityOrigin exists |
| if (createIfNotExists && !SQLiteFileSystem::ensureDatabaseDirectoryExists(originPath)) |
| return String(); |
| |
| // See if we have a path for this database yet |
| if (!m_database.isOpen()) |
| return String(); |
| |
| { |
| auto statement = m_database.prepareStatement("SELECT path FROM Databases WHERE origin=? AND name=?;"_s); |
| if (!statement) |
| return String(); |
| |
| statement->bindText(1, originIdentifier); |
| statement->bindText(2, name); |
| |
| int result = statement->step(); |
| if (result == SQLITE_ROW) |
| return SQLiteFileSystem::appendDatabaseFileNameToPath(originPath, statement->columnText(0)); |
| if (!createIfNotExists) |
| return String(); |
| |
| if (result != SQLITE_DONE) { |
| LOG_ERROR("Failed to retrieve filename from Database Tracker for origin %s, name %s", originIdentifier.utf8().data(), name.utf8().data()); |
| return String(); |
| } |
| } |
| |
| String fileName = generateDatabaseFileName(); |
| |
| if (!addDatabase(origin, name, fileName)) |
| return String(); |
| |
| // If this origin's quota is being tracked (open handle to a database in this origin), add this new database |
| // to the quota manager now |
| String fullFilePath = SQLiteFileSystem::appendDatabaseFileNameToPath(originPath, fileName); |
| |
| return fullFilePath; |
| } |
| |
| String DatabaseTracker::fullPathForDatabase(const SecurityOriginData& origin, const String& name, bool createIfNotExists) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| return fullPathForDatabaseNoLock(origin, name, createIfNotExists).isolatedCopy(); |
| } |
| |
| Vector<SecurityOriginData> DatabaseTracker::origins() |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return { }; |
| |
| auto statement = m_database.prepareStatement("SELECT origin FROM Origins"_s); |
| if (!statement) { |
| LOG_ERROR("Failed to prepare statement."); |
| return { }; |
| } |
| |
| Vector<SecurityOriginData> origins; |
| int stepResult; |
| while ((stepResult = statement->step()) == SQLITE_ROW) |
| origins.append(SecurityOriginData::fromDatabaseIdentifier(statement->columnText(0))->isolatedCopy()); |
| origins.shrinkToFit(); |
| |
| if (stepResult != SQLITE_DONE) |
| LOG_ERROR("Failed to read in all origins from the database."); |
| |
| return origins; |
| } |
| |
| Vector<String> DatabaseTracker::databaseNamesNoLock(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return { }; |
| |
| auto statement = m_database.prepareStatement("SELECT name FROM Databases where origin=?;"_s); |
| if (!statement) |
| return { }; |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| |
| Vector<String> names; |
| int result; |
| while ((result = statement->step()) == SQLITE_ROW) |
| names.append(statement->columnText(0)); |
| names.shrinkToFit(); |
| |
| if (result != SQLITE_DONE) { |
| LOG_ERROR("Failed to retrieve all database names for origin %s", origin.databaseIdentifier().utf8().data()); |
| return { }; |
| } |
| |
| return names; |
| } |
| |
| Vector<String> DatabaseTracker::databaseNames(const SecurityOriginData& origin) |
| { |
| Vector<String> names; |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| names = databaseNamesNoLock(origin); |
| } |
| return isolatedCopy(names); |
| } |
| |
| DatabaseDetails DatabaseTracker::detailsForNameAndOrigin(const String& name, const SecurityOriginData& origin) |
| { |
| String originIdentifier = origin.databaseIdentifier(); |
| String displayName; |
| int64_t expectedUsage; |
| |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return DatabaseDetails(); |
| auto statement = m_database.prepareStatement("SELECT displayName, estimatedSize FROM Databases WHERE origin=? AND name=?"_s); |
| if (!statement) |
| return DatabaseDetails(); |
| |
| statement->bindText(1, originIdentifier); |
| statement->bindText(2, name); |
| |
| int result = statement->step(); |
| if (result == SQLITE_DONE) |
| return DatabaseDetails(); |
| |
| if (result != SQLITE_ROW) { |
| LOG_ERROR("Error retrieving details for database %s in origin %s from tracker database", name.utf8().data(), originIdentifier.utf8().data()); |
| return DatabaseDetails(); |
| } |
| displayName = statement->columnText(0); |
| expectedUsage = statement->columnInt64(1); |
| } |
| |
| String path = fullPathForDatabase(origin, name, false); |
| if (path.isEmpty()) |
| return DatabaseDetails(name, displayName, expectedUsage, 0, std::nullopt, std::nullopt); |
| return DatabaseDetails(name, displayName, expectedUsage, SQLiteFileSystem::databaseFileSize(path), SQLiteFileSystem::databaseCreationTime(path), SQLiteFileSystem::databaseModificationTime(path)); |
| } |
| |
| void DatabaseTracker::setDatabaseDetails(const SecurityOriginData& origin, const String& name, const String& displayName, uint64_t estimatedSize) |
| { |
| String originIdentifier = origin.databaseIdentifier(); |
| int64_t guid = 0; |
| |
| Locker lockDatabase { m_databaseGuard }; |
| |
| openTrackerDatabase(CreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return; |
| |
| { |
| auto statement = m_database.prepareStatement("SELECT guid FROM Databases WHERE origin=? AND name=?"_s); |
| if (!statement) |
| return; |
| |
| statement->bindText(1, originIdentifier); |
| statement->bindText(2, name); |
| |
| int result = statement->step(); |
| if (result == SQLITE_ROW) |
| guid = statement->columnInt64(0); |
| |
| if (!guid) { |
| if (result != SQLITE_DONE) |
| LOG_ERROR("Error to determing existence of database %s in origin %s in tracker database", name.utf8().data(), originIdentifier.utf8().data()); |
| else { |
| // This case should never occur - we should never be setting database details for a database that doesn't already exist in the tracker |
| // But since the tracker file is an external resource not under complete control of our code, it's somewhat invalid to make this an ASSERT case |
| // So we'll print an error instead |
| LOG_ERROR("Could not retrieve guid for database %s in origin %s from the tracker database - it is invalid to set database details on a database that doesn't already exist in the tracker", name.utf8().data(), originIdentifier.utf8().data()); |
| } |
| return; |
| } |
| } |
| |
| auto updateStatement = m_database.prepareStatement("UPDATE Databases SET displayName=?, estimatedSize=? WHERE guid=?"_s); |
| if (!updateStatement) |
| return; |
| |
| updateStatement->bindText(1, displayName); |
| updateStatement->bindInt64(2, estimatedSize); |
| updateStatement->bindInt64(3, guid); |
| |
| if (updateStatement->step() != SQLITE_DONE) { |
| LOG_ERROR("Failed to update details for database %s in origin %s", name.utf8().data(), originIdentifier.utf8().data()); |
| return; |
| } |
| |
| if (m_client) |
| m_client->dispatchDidModifyDatabase(origin, name); |
| } |
| |
| void DatabaseTracker::doneCreatingDatabase(Database& database) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| doneCreatingDatabase(database.securityOrigin(), database.stringIdentifierIsolatedCopy()); |
| } |
| |
| Vector<Ref<Database>> DatabaseTracker::openDatabases() |
| { |
| Vector<Ref<Database>> openDatabases; |
| { |
| Locker openDatabaseMapLock { m_openDatabaseMapGuard }; |
| |
| if (m_openDatabaseMap) { |
| for (auto& nameMap : m_openDatabaseMap->values()) { |
| for (auto& set : nameMap->values()) { |
| for (auto& database : *set) |
| openDatabases.append(*database); |
| } |
| } |
| } |
| } |
| return openDatabases; |
| } |
| |
| void DatabaseTracker::addOpenDatabase(Database& database) |
| { |
| Locker openDatabaseMapLock { m_openDatabaseMapGuard }; |
| |
| if (!m_openDatabaseMap) |
| m_openDatabaseMap = makeUnique<DatabaseOriginMap>(); |
| |
| auto origin = database.securityOrigin(); |
| |
| auto* nameMap = m_openDatabaseMap->get(origin); |
| if (!nameMap) { |
| nameMap = new DatabaseNameMap; |
| m_openDatabaseMap->add(origin.isolatedCopy(), nameMap); |
| } |
| |
| String name = database.stringIdentifierIsolatedCopy(); |
| auto* databaseSet = nameMap->get(name); |
| if (!databaseSet) { |
| databaseSet = new DatabaseSet; |
| nameMap->set(name.isolatedCopy(), databaseSet); |
| } |
| |
| databaseSet->add(&database); |
| |
| LOG(StorageAPI, "Added open Database %s (%p)\n", database.stringIdentifierIsolatedCopy().utf8().data(), &database); |
| } |
| |
| void DatabaseTracker::removeOpenDatabase(Database& database) |
| { |
| Locker openDatabaseMapLock { m_openDatabaseMapGuard }; |
| |
| if (!m_openDatabaseMap) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| DatabaseNameMap* nameMap = m_openDatabaseMap->get(database.securityOrigin()); |
| if (!nameMap) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| String name = database.stringIdentifierIsolatedCopy(); |
| auto* databaseSet = nameMap->get(name); |
| if (!databaseSet) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| databaseSet->remove(&database); |
| |
| LOG(StorageAPI, "Removed open Database %s (%p)\n", database.stringIdentifierIsolatedCopy().utf8().data(), &database); |
| |
| if (!databaseSet->isEmpty()) |
| return; |
| |
| nameMap->remove(name); |
| delete databaseSet; |
| |
| if (!nameMap->isEmpty()) |
| return; |
| |
| m_openDatabaseMap->remove(database.securityOrigin()); |
| delete nameMap; |
| } |
| |
| RefPtr<OriginLock> DatabaseTracker::originLockFor(const SecurityOriginData& origin) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| String databaseIdentifier = origin.databaseIdentifier(); |
| |
| // The originLockMap is accessed from multiple DatabaseThreads since |
| // different script contexts can be writing to different databases from |
| // the same origin. Hence, the databaseIdentifier key needs to be an |
| // isolated copy. An isolated copy gives us a value whose refCounting is |
| // thread-safe, since our copy is guarded by the m_databaseGuard mutex. |
| databaseIdentifier = databaseIdentifier.isolatedCopy(); |
| |
| OriginLockMap::AddResult addResult = |
| m_originLockMap.add(databaseIdentifier, RefPtr<OriginLock>()); |
| if (!addResult.isNewEntry) |
| return addResult.iterator->value; |
| |
| String path = originPath(origin); |
| RefPtr<OriginLock> lock = adoptRef(*new OriginLock(path)); |
| ASSERT(lock); |
| addResult.iterator->value = lock; |
| |
| return lock; |
| } |
| |
| void DatabaseTracker::deleteOriginLockFor(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| // There is not always an instance of an OriginLock associated with an origin. |
| // For example, if the OriginLock lock file was created by a previous run of |
| // the browser which has now terminated, and the current browser process |
| // has not executed any database transactions from this origin that would |
| // have created the OriginLock instance in memory. In this case, we will have |
| // a lock file but not an OriginLock instance in memory. |
| |
| // This function is only called if we are already deleting all the database |
| // files in this origin. We'll give the OriginLock one chance to do an |
| // orderly clean up first when we remove its ref from the m_originLockMap. |
| // This may or may not be possible depending on whether other threads are |
| // also using the OriginLock at the same time. After that, we will delete the lock file. |
| |
| m_originLockMap.remove(origin.databaseIdentifier()); |
| OriginLock::deleteLockFile(originPath(origin)); |
| } |
| |
| uint64_t DatabaseTracker::usage(const SecurityOriginData& origin) |
| { |
| String originPath = this->originPath(origin); |
| uint64_t diskUsage = 0; |
| for (auto& fileName : FileSystem::listDirectory(originPath)) { |
| if (fileName.endsWith(".db")) |
| diskUsage += SQLiteFileSystem::databaseFileSize(FileSystem::pathByAppendingComponent(originPath, fileName)); |
| } |
| return diskUsage; |
| } |
| |
| uint64_t DatabaseTracker::quotaNoLock(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| uint64_t quota = 0; |
| |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return quota; |
| |
| auto statement = m_database.prepareStatement("SELECT quota FROM Origins where origin=?;"_s); |
| if (!statement) { |
| LOG_ERROR("Failed to prepare statement."); |
| return quota; |
| } |
| statement->bindText(1, origin.databaseIdentifier()); |
| |
| if (statement->step() == SQLITE_ROW) |
| quota = statement->columnInt64(0); |
| |
| return quota; |
| } |
| |
| uint64_t DatabaseTracker::quota(const SecurityOriginData& origin) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| return quotaNoLock(origin); |
| } |
| |
| void DatabaseTracker::setQuota(const SecurityOriginData& origin, uint64_t quota) |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| |
| if (quotaNoLock(origin) == quota) |
| return; |
| |
| openTrackerDatabase(CreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return; |
| |
| bool insertedNewOrigin = false; |
| |
| bool originEntryExists = hasEntryForOriginNoLock(origin); |
| if (!originEntryExists) { |
| auto statement = m_database.prepareStatement("INSERT INTO Origins VALUES (?, ?)"_s); |
| if (!statement) { |
| LOG_ERROR("Unable to establish origin %s in the tracker", origin.databaseIdentifier().utf8().data()); |
| } else { |
| statement->bindText(1, origin.databaseIdentifier()); |
| statement->bindInt64(2, quota); |
| |
| if (statement->step() != SQLITE_DONE) |
| LOG_ERROR("Unable to establish origin %s in the tracker", origin.databaseIdentifier().utf8().data()); |
| else |
| insertedNewOrigin = true; |
| } |
| } else { |
| auto statement = m_database.prepareStatement("UPDATE Origins SET quota=? WHERE origin=?"_s); |
| bool error = !statement; |
| if (!error) { |
| statement->bindInt64(1, quota); |
| statement->bindText(2, origin.databaseIdentifier()); |
| |
| error = !statement->executeCommand(); |
| } |
| |
| if (error) |
| LOG_ERROR("Failed to set quota %llu in tracker database for origin %s", quota, origin.databaseIdentifier().utf8().data()); |
| } |
| |
| if (m_client) { |
| if (insertedNewOrigin) |
| m_client->dispatchDidAddNewOrigin(); |
| m_client->dispatchDidModifyOrigin(origin); |
| } |
| } |
| |
| bool DatabaseTracker::addDatabase(const SecurityOriginData& origin, const String& name, const String& path) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| openTrackerDatabase(CreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return false; |
| |
| // New database should never be added until the origin has been established |
| ASSERT(hasEntryForOriginNoLock(origin)); |
| |
| auto statement = m_database.prepareStatement("INSERT INTO Databases (origin, name, path) VALUES (?, ?, ?);"_s); |
| |
| if (!statement) |
| return false; |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| statement->bindText(2, name); |
| statement->bindText(3, path); |
| |
| if (!statement->executeCommand()) { |
| LOG_ERROR("Failed to add database %s to origin %s: %s\n", name.utf8().data(), origin.databaseIdentifier().utf8().data(), m_database.lastErrorMsg()); |
| return false; |
| } |
| |
| if (m_client) |
| m_client->dispatchDidModifyOrigin(origin); |
| |
| return true; |
| } |
| |
| void DatabaseTracker::deleteAllDatabasesImmediately() |
| { |
| // This method is only intended for use by DumpRenderTree / WebKitTestRunner. |
| // Actually deleting the databases is necessary to reset to a known state before running |
| // each test case, but may be unsafe in deployment use cases (where multiple applications |
| // may be accessing the same databases concurrently). |
| for (auto& origin : origins()) |
| deleteOrigin(origin, DeletionMode::Immediate); |
| } |
| |
| void DatabaseTracker::deleteDatabasesModifiedSince(WallTime time) |
| { |
| for (auto& origin : origins()) { |
| Vector<String> databaseNames = this->databaseNames(origin); |
| Vector<String> databaseNamesToDelete; |
| databaseNamesToDelete.reserveInitialCapacity(databaseNames.size()); |
| for (const auto& databaseName : databaseNames) { |
| auto fullPath = fullPathForDatabase(origin, databaseName, false); |
| |
| // If the file doesn't exist, we previously deleted it but failed to remove the information |
| // from the tracker database. We want to delete all of the information associated with this |
| // database from the tracker database, so still add its name to databaseNamesToDelete. |
| if (FileSystem::fileExists(fullPath)) { |
| auto modificationTime = FileSystem::fileModificationTime(fullPath); |
| if (!modificationTime) |
| continue; |
| |
| if (modificationTime.value() < time) |
| continue; |
| } |
| |
| databaseNamesToDelete.uncheckedAppend(databaseName); |
| } |
| |
| if (databaseNames.size() == databaseNamesToDelete.size()) |
| deleteOrigin(origin); |
| else { |
| for (const auto& databaseName : databaseNamesToDelete) |
| deleteDatabase(origin, databaseName); |
| } |
| } |
| } |
| |
| // It is the caller's responsibility to make sure that nobody is trying to create, delete, open, or close databases in this origin while the deletion is |
| // taking place. |
| bool DatabaseTracker::deleteOrigin(const SecurityOriginData& origin) |
| { |
| return deleteOrigin(origin, DeletionMode::Default); |
| } |
| |
| bool DatabaseTracker::deleteOrigin(const SecurityOriginData& origin, DeletionMode deletionMode) |
| { |
| Vector<String> databaseNames; |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return false; |
| |
| databaseNames = databaseNamesNoLock(origin); |
| if (databaseNames.isEmpty()) |
| LOG_ERROR("Unable to retrieve list of database names for origin %s", origin.databaseIdentifier().utf8().data()); |
| |
| if (!canDeleteOrigin(origin)) { |
| LOG_ERROR("Tried to delete an origin (%s) while either creating database in it or already deleting it", origin.databaseIdentifier().utf8().data()); |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| recordDeletingOrigin(origin); |
| } |
| |
| // We drop the lock here because holding locks during a call to deleteDatabaseFile will deadlock. |
| bool failedToDeleteAnyDatabaseFile = false; |
| for (auto& name : databaseNames) { |
| if (FileSystem::fileExists(fullPathForDatabase(origin, name, false)) && !deleteDatabaseFile(origin, name, deletionMode)) { |
| // Even if the file can't be deleted, we want to try and delete the rest, don't return early here. |
| LOG_ERROR("Unable to delete file for database %s in origin %s", name.utf8().data(), origin.databaseIdentifier().utf8().data()); |
| failedToDeleteAnyDatabaseFile = true; |
| } |
| } |
| |
| // If databaseNames is empty, delete everything in the directory containing the databases for this origin. |
| // This condition indicates that we previously tried to remove the origin but didn't get all of the way |
| // through the deletion process. Because we have lost track of the databases for this origin, |
| // we can assume that no other process is accessing them. This means it should be safe to delete them outright. |
| if (databaseNames.isEmpty()) { |
| #if PLATFORM(COCOA) |
| RELEASE_LOG_ERROR(DatabaseTracker, "Unable to retrieve list of database names for origin"); |
| #endif |
| auto originPath = this->originPath(origin); |
| for (const auto& fileName : FileSystem::listDirectory(originPath)) { |
| if (!FileSystem::deleteFile(FileSystem::pathByAppendingComponent(originPath, fileName))) |
| failedToDeleteAnyDatabaseFile = true; |
| } |
| } |
| |
| // If we failed to delete any database file, don't remove the origin from the tracker |
| // database because we didn't successfully remove all of its data. |
| if (failedToDeleteAnyDatabaseFile) { |
| #if PLATFORM(COCOA) |
| RELEASE_LOG_ERROR(DatabaseTracker, "Failed to delete database for origin"); |
| #endif |
| return false; |
| } |
| |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| deleteOriginLockFor(origin); |
| doneDeletingOrigin(origin); |
| |
| SQLiteTransaction transaction(m_database); |
| transaction.begin(); |
| |
| { |
| auto statement = m_database.prepareStatement("DELETE FROM Databases WHERE origin=?"_s); |
| if (!statement) { |
| LOG_ERROR("Unable to prepare deletion of databases from origin %s from tracker", origin.databaseIdentifier().utf8().data()); |
| return false; |
| } |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| |
| if (!statement->executeCommand()) { |
| LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin.databaseIdentifier().utf8().data()); |
| return false; |
| } |
| |
| auto originStatement = m_database.prepareStatement("DELETE FROM Origins WHERE origin=?"_s); |
| if (!originStatement) { |
| LOG_ERROR("Unable to prepare deletion of origin %s from tracker", origin.databaseIdentifier().utf8().data()); |
| return false; |
| } |
| |
| originStatement->bindText(1, origin.databaseIdentifier()); |
| |
| if (!originStatement->executeCommand()) { |
| LOG_ERROR("Unable to execute deletion of databases from origin %s from tracker", origin.databaseIdentifier().utf8().data()); |
| return false; |
| } |
| } |
| |
| transaction.commit(); |
| |
| SQLiteFileSystem::deleteEmptyDatabaseDirectory(originPath(origin)); |
| |
| bool isEmpty = true; |
| |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (m_database.isOpen()) { |
| auto statement = m_database.prepareStatement("SELECT origin FROM Origins"_s); |
| if (!statement) |
| LOG_ERROR("Failed to prepare statement."); |
| else if (statement->step() == SQLITE_ROW) |
| isEmpty = false; |
| } |
| |
| // If we removed the last origin, do some additional deletion. |
| if (isEmpty) { |
| if (m_database.isOpen()) |
| m_database.close(); |
| SQLiteFileSystem::deleteDatabaseFile(trackerDatabasePath()); |
| SQLiteFileSystem::deleteEmptyDatabaseDirectory(m_databaseDirectoryPath); |
| } |
| |
| if (m_client) { |
| m_client->dispatchDidModifyOrigin(origin); |
| m_client->dispatchDidDeleteDatabaseOrigin(); |
| for (auto& name : databaseNames) |
| m_client->dispatchDidModifyDatabase(origin, name); |
| } |
| } |
| return true; |
| } |
| |
| bool DatabaseTracker::isDeletingDatabaseOrOriginFor(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| // Can't create a database while someone else is deleting it; there's a risk of leaving untracked database debris on the disk. |
| return isDeletingDatabase(origin, name) || isDeletingOrigin(origin); |
| } |
| |
| void DatabaseTracker::recordCreatingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| // We don't use HashMap::ensure here to avoid making an isolated copy of the origin every time. |
| auto* nameSet = m_beingCreated.get(origin); |
| if (!nameSet) { |
| auto ownedSet = makeUnique<HashCountedSet<String>>(); |
| nameSet = ownedSet.get(); |
| m_beingCreated.add(origin.isolatedCopy(), WTFMove(ownedSet)); |
| } |
| nameSet->add(name.isolatedCopy()); |
| } |
| |
| void DatabaseTracker::doneCreatingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| ASSERT(m_beingCreated.contains(origin)); |
| |
| auto iterator = m_beingCreated.find(origin); |
| if (iterator == m_beingCreated.end()) |
| return; |
| |
| auto& countedSet = *iterator->value; |
| ASSERT(countedSet.contains(name)); |
| |
| if (countedSet.remove(name) && countedSet.isEmpty()) |
| m_beingCreated.remove(iterator); |
| } |
| |
| bool DatabaseTracker::creatingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| |
| auto iterator = m_beingCreated.find(origin); |
| return iterator != m_beingCreated.end() && iterator->value->contains(name); |
| } |
| |
| bool DatabaseTracker::canDeleteDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| return !creatingDatabase(origin, name) && !isDeletingDatabase(origin, name); |
| } |
| |
| void DatabaseTracker::recordDeletingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| ASSERT(canDeleteDatabase(origin, name)); |
| |
| // We don't use HashMap::ensure here to avoid making an isolated copy of the origin every time. |
| auto* nameSet = m_beingDeleted.get(origin); |
| if (!nameSet) { |
| auto ownedSet = makeUnique<HashSet<String>>(); |
| nameSet = ownedSet.get(); |
| m_beingDeleted.add(origin.isolatedCopy(), WTFMove(ownedSet)); |
| } |
| ASSERT(!nameSet->contains(name)); |
| nameSet->add(name.isolatedCopy()); |
| } |
| |
| void DatabaseTracker::doneDeletingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| ASSERT(m_beingDeleted.contains(origin)); |
| |
| auto iterator = m_beingDeleted.find(origin); |
| if (iterator == m_beingDeleted.end()) |
| return; |
| |
| ASSERT(iterator->value->contains(name)); |
| iterator->value->remove(name); |
| if (iterator->value->isEmpty()) |
| m_beingDeleted.remove(iterator); |
| } |
| |
| bool DatabaseTracker::isDeletingDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| auto* nameSet = m_beingDeleted.get(origin); |
| return nameSet && nameSet->contains(name); |
| } |
| |
| bool DatabaseTracker::canDeleteOrigin(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| return !(isDeletingOrigin(origin) || m_beingCreated.get(origin)); |
| } |
| |
| bool DatabaseTracker::isDeletingOrigin(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| return m_originsBeingDeleted.contains(origin); |
| } |
| |
| void DatabaseTracker::recordDeletingOrigin(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| ASSERT(!isDeletingOrigin(origin)); |
| m_originsBeingDeleted.add(origin.isolatedCopy()); |
| } |
| |
| void DatabaseTracker::doneDeletingOrigin(const SecurityOriginData& origin) |
| { |
| ASSERT(m_databaseGuard.isHeld()); |
| ASSERT(isDeletingOrigin(origin)); |
| m_originsBeingDeleted.remove(origin); |
| } |
| |
| bool DatabaseTracker::deleteDatabase(const SecurityOriginData& origin, const String& name) |
| { |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| if (!m_database.isOpen()) |
| return false; |
| |
| if (!canDeleteDatabase(origin, name)) { |
| ASSERT_NOT_REACHED(); |
| return false; |
| } |
| recordDeletingDatabase(origin, name); |
| } |
| |
| // We drop the lock here because holding locks during a call to deleteDatabaseFile will deadlock. |
| if (FileSystem::fileExists(fullPathForDatabase(origin, name, false)) && !deleteDatabaseFile(origin, name, DeletionMode::Default)) { |
| LOG_ERROR("Unable to delete file for database %s in origin %s", name.utf8().data(), origin.databaseIdentifier().utf8().data()); |
| Locker lockDatabase { m_databaseGuard }; |
| doneDeletingDatabase(origin, name); |
| return false; |
| } |
| |
| Locker lockDatabase { m_databaseGuard }; |
| |
| auto statement = m_database.prepareStatement("DELETE FROM Databases WHERE origin=? AND name=?"_s); |
| if (!statement) { |
| LOG_ERROR("Unable to prepare deletion of database %s from origin %s from tracker", name.utf8().data(), origin.databaseIdentifier().utf8().data()); |
| doneDeletingDatabase(origin, name); |
| return false; |
| } |
| |
| statement->bindText(1, origin.databaseIdentifier()); |
| statement->bindText(2, name); |
| |
| if (!statement->executeCommand()) { |
| LOG_ERROR("Unable to execute deletion of database %s from origin %s from tracker", name.utf8().data(), origin.databaseIdentifier().utf8().data()); |
| doneDeletingDatabase(origin, name); |
| return false; |
| } |
| |
| if (m_client) { |
| m_client->dispatchDidModifyOrigin(origin); |
| m_client->dispatchDidModifyDatabase(origin, name); |
| m_client->dispatchDidDeleteDatabase(); |
| } |
| doneDeletingDatabase(origin, name); |
| |
| return true; |
| } |
| |
| // deleteDatabaseFile has to release locks between looking up the list of databases to close and closing them. While this is in progress, the caller |
| // is responsible for making sure no new databases are opened in the file to be deleted. |
| bool DatabaseTracker::deleteDatabaseFile(const SecurityOriginData& origin, const String& name, DeletionMode deletionMode) |
| { |
| String fullPath = fullPathForDatabase(origin, name, false); |
| if (fullPath.isEmpty()) |
| return true; |
| |
| #ifndef NDEBUG |
| { |
| Locker lockDatabase { m_databaseGuard }; |
| ASSERT(isDeletingDatabaseOrOriginFor(origin, name)); |
| } |
| #endif |
| |
| Vector<Ref<Database>> deletedDatabases; |
| |
| // Make sure not to hold the any locks when calling |
| // Database::markAsDeletedAndClose(), since that can cause a deadlock |
| // during the synchronous DatabaseThread call it triggers. |
| { |
| Locker openDatabaseMapLock { m_openDatabaseMapGuard }; |
| if (m_openDatabaseMap) { |
| if (auto* nameMap = m_openDatabaseMap->get(origin)) { |
| if (auto* databaseSet = nameMap->get(name)) { |
| for (auto& database : *databaseSet) |
| deletedDatabases.append(*database); |
| } |
| } |
| } |
| } |
| |
| for (auto& database : deletedDatabases) |
| database->markAsDeletedAndClose(); |
| |
| #if PLATFORM(IOS_FAMILY) |
| if (deletionMode == DeletionMode::Deferred) { |
| // Other background processes may still be accessing this database. Deleting the database directly |
| // would nuke the POSIX file locks, potentially causing Safari/WebApp to corrupt the new db if it's running in the background. |
| // We'll instead truncate the database file to 0 bytes. If another process is operating on this same database file after |
| // the truncation, it should get an error since the database file is no longer valid. When Safari is launched |
| // next time, it'll go through the database files and clean up any zero-bytes ones. |
| SQLiteDatabase database; |
| if (!database.open(fullPath)) |
| return false; |
| return SQLiteFileSystem::truncateDatabaseFile(database.sqlite3Handle()); |
| } |
| #else |
| UNUSED_PARAM(deletionMode); |
| #endif |
| |
| return SQLiteFileSystem::deleteDatabaseFile(fullPath); |
| } |
| |
| #if PLATFORM(IOS_FAMILY) |
| |
| // FIXME: This uses m_database without locking m_databaseGuard. |
| void DatabaseTracker::removeDeletedOpenedDatabases() WTF_IGNORES_THREAD_SAFETY_ANALYSIS |
| { |
| // This is called when another app has deleted a database. Go through all opened databases in this |
| // tracker and close any that's no longer being tracked in the database. |
| |
| { |
| // Acquire the lock before calling openTrackerDatabase. |
| Locker lockDatabase { m_databaseGuard }; |
| openTrackerDatabase(DontCreateIfDoesNotExist); |
| |
| if (!m_database.isOpen()) |
| return; |
| } |
| |
| // Keep track of which opened databases have been deleted. |
| Vector<RefPtr<Database>> deletedDatabases; |
| Vector<std::pair<SecurityOriginData, Vector<String>>> deletedDatabaseNames; |
| |
| // Make sure not to hold the m_openDatabaseMapGuard mutex when calling |
| // Database::markAsDeletedAndClose(), since that can cause a deadlock |
| // during the synchronous DatabaseThread call it triggers. |
| { |
| Locker openDatabaseMapLock { m_openDatabaseMapGuard }; |
| if (m_openDatabaseMap) { |
| for (auto& openDatabase : *m_openDatabaseMap) { |
| auto& origin = openDatabase.key; |
| DatabaseNameMap* databaseNameMap = openDatabase.value; |
| Vector<String> deletedDatabaseNamesForThisOrigin; |
| |
| // Loop through all opened databases in this origin. Get the current database file path of each database and see if |
| // it still matches the path stored in the opened database object. |
| for (auto& databases : *databaseNameMap) { |
| String databaseName = databases.key; |
| String databaseFileName; |
| if (auto statement = m_database.prepareStatement("SELECT path FROM Databases WHERE origin=? AND name=?;"_s)) { |
| statement->bindText(1, origin.databaseIdentifier()); |
| statement->bindText(2, databaseName); |
| if (statement->step() == SQLITE_ROW) |
| databaseFileName = statement->columnText(0); |
| } |
| |
| bool foundDeletedDatabase = false; |
| for (auto& db : *databases.value) { |
| // We are done if this database has already been marked as deleted. |
| if (db->deleted()) |
| continue; |
| |
| // If this database has been deleted or if its database file no longer matches the current version, this database is no longer valid and it should be marked as deleted. |
| if (databaseFileName.isNull() || databaseFileName != FileSystem::pathFileName(db->fileNameIsolatedCopy())) { |
| deletedDatabases.append(db); |
| foundDeletedDatabase = true; |
| } |
| } |
| |
| // If the database no longer exists, we should remember to send that information to the client later. |
| if (m_client && foundDeletedDatabase && databaseFileName.isNull()) |
| deletedDatabaseNamesForThisOrigin.append(databaseName); |
| } |
| |
| if (!deletedDatabaseNamesForThisOrigin.isEmpty()) |
| deletedDatabaseNames.append({ origin, WTFMove(deletedDatabaseNamesForThisOrigin) }); |
| } |
| } |
| } |
| |
| for (auto& deletedDatabase : deletedDatabases) |
| deletedDatabase->markAsDeletedAndClose(); |
| |
| for (auto& deletedDatabase : deletedDatabaseNames) { |
| auto& origin = deletedDatabase.first; |
| m_client->dispatchDidModifyOrigin(origin); |
| for (auto& databaseName : deletedDatabase.second) |
| m_client->dispatchDidModifyDatabase(origin, databaseName); |
| } |
| } |
| |
| static bool isZeroByteFile(const String& path) |
| { |
| auto size = FileSystem::fileSize(path); |
| return size && !*size; |
| } |
| |
| bool DatabaseTracker::deleteDatabaseFileIfEmpty(const String& path) |
| { |
| if (!isZeroByteFile(path)) |
| return false; |
| |
| SQLiteDatabase database; |
| if (!database.open(path)) |
| return false; |
| |
| // Specify that we want the exclusive locking mode, so after the next write, |
| // we'll be holding the lock to this database file. |
| { |
| auto lockStatement = database.prepareStatement("PRAGMA locking_mode=EXCLUSIVE;"_s); |
| if (!lockStatement) |
| return false; |
| int result = lockStatement->step(); |
| if (result != SQLITE_ROW && result != SQLITE_DONE) |
| return false; |
| } |
| |
| if (!database.executeCommand("BEGIN EXCLUSIVE TRANSACTION;"_s)) |
| return false; |
| |
| // At this point, we hold the exclusive lock to this file. |
| // Check that the database doesn't contain any tables. |
| if (!database.executeCommand("SELECT name FROM sqlite_master WHERE type='table';"_s)) |
| return false; |
| |
| database.executeCommand("COMMIT TRANSACTION;"_s); |
| |
| database.close(); |
| |
| return SQLiteFileSystem::deleteDatabaseFile(path); |
| } |
| |
| static Lock openDatabaseLock; |
| Lock& DatabaseTracker::openDatabaseMutex() |
| { |
| return openDatabaseLock; |
| } |
| |
| // We are not using WTF_ACQUIRES_LOCK(openDatabaseLock) because the call sites are ObjC functions and cannot be annotated. |
| void DatabaseTracker::emptyDatabaseFilesRemovalTaskWillBeScheduled() WTF_IGNORES_THREAD_SAFETY_ANALYSIS |
| { |
| // Lock the database from opening any database until we are done with scanning the file system for |
| // zero byte database files to remove. |
| openDatabaseLock.lock(); |
| } |
| |
| // We are not using WTF_RELEASES_LOCK(openDatabaseLock) because the call sites are ObjC functions and cannot be annotated. |
| void DatabaseTracker::emptyDatabaseFilesRemovalTaskDidFinish() WTF_IGNORES_THREAD_SAFETY_ANALYSIS |
| { |
| openDatabaseLock.unlock(); |
| } |
| |
| #endif |
| |
| void DatabaseTracker::setClient(DatabaseManagerClient* client) |
| { |
| m_client = client; |
| } |
| |
| static Lock notificationLock; |
| |
| using NotificationQueue = Vector<std::pair<SecurityOriginData, String>>; |
| |
| static NotificationQueue& notificationQueue() |
| { |
| static NeverDestroyed<NotificationQueue> queue; |
| return queue; |
| } |
| |
| void DatabaseTracker::scheduleNotifyDatabaseChanged(const SecurityOriginData& origin, const String& name) |
| { |
| Locker locker { notificationLock }; |
| notificationQueue().append(std::make_pair(origin.isolatedCopy(), name.isolatedCopy())); |
| scheduleForNotification(); |
| } |
| |
| static bool notificationScheduled = false; |
| |
| void DatabaseTracker::scheduleForNotification() |
| { |
| ASSERT(notificationLock.isHeld()); |
| |
| if (!notificationScheduled) { |
| callOnMainThread([] { |
| notifyDatabasesChanged(); |
| }); |
| notificationScheduled = true; |
| } |
| } |
| |
| void DatabaseTracker::notifyDatabasesChanged() |
| { |
| // Note that if DatabaseTracker ever becomes non-singleton, we'll have to amend this notification |
| // mechanism to include which tracker the notification goes out on as well. |
| auto& tracker = DatabaseTracker::singleton(); |
| |
| NotificationQueue notifications; |
| { |
| Locker locker { notificationLock }; |
| notifications.swap(notificationQueue()); |
| notificationScheduled = false; |
| } |
| |
| if (!tracker.m_client) |
| return; |
| |
| for (auto& notification : notifications) |
| tracker.m_client->dispatchDidModifyDatabase(notification.first, notification.second); |
| } |
| |
| |
| } // namespace WebCore |