blob: 08c0682cceaf533ad8edf1eab7ae08f7c2315a01 [file] [log] [blame]
/*
* Copyright (C) 2022 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.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. 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 INC. 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 "PushDatabase.h"
#if ENABLE(SERVICE_WORKER)
#include "Logging.h"
#include "SQLValue.h"
#include "SQLiteFileSystem.h"
#include "SQLiteTransaction.h"
#include "SecurityOrigin.h"
#include <iterator>
#include <wtf/CrossThreadCopier.h>
#include <wtf/Expected.h>
#include <wtf/FileSystem.h>
#include <wtf/RunLoop.h>
#include <wtf/Scope.h>
#include <wtf/UniqueRef.h>
#include <wtf/text/StringConcatenateNumbers.h>
#define PUSHDB_RELEASE_LOG(fmt, ...) RELEASE_LOG(Push, "%p - PushDatabase::" fmt, this, ##__VA_ARGS__)
#define PUSHDB_RELEASE_LOG_ERROR(fmt, ...) RELEASE_LOG_ERROR(Push, "%p - PushDatabase::" fmt, this, ##__VA_ARGS__)
#define PUSHDB_RELEASE_LOG_BIND_ERROR() PUSHDB_RELEASE_LOG_ERROR("Failed to bind statement (%d): %s", m_db->lastError(), m_db->lastErrorMsg())
#define kPushRecordColumns " sub.rowID, ss.bundleID, ss.securityOrigin, sub.scope, sub.endpoint, sub.topic, sub.serverVAPIDPublicKey, sub.clientPublicKey, sub.clientPrivateKey, sub.sharedAuthSecret, sub.expirationTime "
namespace WebCore {
static constexpr ASCIILiteral pushDatabaseSchemaV1Statements[] = {
"PRAGMA auto_vacuum=INCREMENTAL"_s,
};
static constexpr ASCIILiteral pushDatabaseSchemaV2Statements[] = {
"CREATE TABLE SubscriptionSets("
" rowID INTEGER PRIMARY KEY AUTOINCREMENT,"
" creationTime INT NOT NULL,"
" bundleID TEXT NOT NULL,"
" securityOrigin TEXT NOT NULL,"
" silentPushCount INT NOT NULL,"
" UNIQUE(bundleID, securityOrigin))"_s,
"CREATE TABLE Subscriptions("
" rowID INTEGER PRIMARY KEY AUTOINCREMENT,"
" creationTime INT NOT NULL,"
" subscriptionSetID INT NOT NULL,"
" scope TEXT NOT NULL,"
" endpoint TEXT NOT NULL,"
" topic TEXT NOT NULL UNIQUE,"
" serverVAPIDPublicKey BLOB NOT NULL,"
" clientPublicKey BLOB NOT NULL,"
" clientPrivateKey BLOB NOT NULL,"
" sharedAuthSecret BLOB NOT NULL,"
" expirationTime INT,"
" UNIQUE(scope, subscriptionSetID))"_s,
"CREATE INDEX Subscriptions_SubscriptionSetID_Index ON Subscriptions(subscriptionSetID)"_s,
};
static constexpr ASCIILiteral pushDatabaseSchemaV3Statements[] = {
"CREATE TABLE Metadata(key TEXT, value, UNIQUE(key))"_s,
};
static constexpr Span<const ASCIILiteral> pushDatabaseSchemaStatements[] = {
{ pushDatabaseSchemaV1Statements },
{ pushDatabaseSchemaV2Statements },
{ pushDatabaseSchemaV3Statements },
};
static constexpr int currentPushDatabaseVersion = std::size(pushDatabaseSchemaStatements);
static constexpr ASCIILiteral publicTokenKey = "publicToken"_s;
PushRecord PushRecord::isolatedCopy() const &
{
return {
identifier,
bundleID.isolatedCopy(),
securityOrigin.isolatedCopy(),
scope.isolatedCopy(),
endpoint.isolatedCopy(),
topic.isolatedCopy(),
serverVAPIDPublicKey,
clientPublicKey,
clientPrivateKey,
sharedAuthSecret,
expirationTime
};
}
PushRecord PushRecord::isolatedCopy() &&
{
return {
identifier,
WTFMove(bundleID).isolatedCopy(),
WTFMove(securityOrigin).isolatedCopy(),
WTFMove(scope).isolatedCopy(),
WTFMove(endpoint).isolatedCopy(),
WTFMove(topic).isolatedCopy(),
WTFMove(serverVAPIDPublicKey),
WTFMove(clientPublicKey),
WTFMove(clientPrivateKey),
WTFMove(sharedAuthSecret),
expirationTime
};
}
RemovedPushRecord RemovedPushRecord::isolatedCopy() const &
{
return { identifier, topic.isolatedCopy(), serverVAPIDPublicKey };
}
RemovedPushRecord RemovedPushRecord::isolatedCopy() &&
{
return { identifier, WTFMove(topic).isolatedCopy(), WTFMove(serverVAPIDPublicKey) };
}
enum class ShouldDeleteAndRetry { No, Yes };
static Expected<UniqueRef<SQLiteDatabase>, ShouldDeleteAndRetry> openAndMigrateDatabaseImpl(const String& path)
{
ASSERT(!RunLoop::isMain());
if (path != ":memory:"_s && !FileSystem::fileExists(path) && !FileSystem::makeAllDirectories(FileSystem::parentPath(path))) {
RELEASE_LOG_ERROR(Push, "Couldn't create PushDatabase parent directories for path %s", path.utf8().data());
return makeUnexpected(ShouldDeleteAndRetry::No);
}
auto db = WTF::makeUniqueRef<SQLiteDatabase>();
db->disableThreadingChecks();
if (!db->open(path)) {
RELEASE_LOG_ERROR(Push, "Couldn't open PushDatabase at path %s", path.utf8().data());
return makeUnexpected(ShouldDeleteAndRetry::Yes);
}
int version = 0;
{
auto sql = db->prepareStatement("PRAGMA user_version"_s);
if (!sql || sql->step() != SQLITE_ROW) {
RELEASE_LOG_ERROR(Push, "Couldn't get PushDatabase version at path %s", path.utf8().data());
return makeUnexpected(ShouldDeleteAndRetry::Yes);
}
version = sql->columnInt(0);
}
if (version < 0 || version > currentPushDatabaseVersion) {
RELEASE_LOG_ERROR(Push, "Found unexpected PushDatabase version: %d (expected: %d) at path: %s", version, currentPushDatabaseVersion, path.utf8().data());
return makeUnexpected(ShouldDeleteAndRetry::Yes);
}
if (version < currentPushDatabaseVersion) {
SQLiteTransaction transaction(db);
transaction.begin();
for (auto i = version; i < currentPushDatabaseVersion; i++) {
for (auto statement : pushDatabaseSchemaStatements[i]) {
if (!db->executeCommand(statement)) {
RELEASE_LOG_ERROR(Push, "Error executing PushDatabase DDL statement %s at path %s: %d", statement.characters(), path.utf8().data(), db->lastError());
return makeUnexpected(ShouldDeleteAndRetry::Yes);
}
}
}
if (!db->executeCommandSlow(makeString("PRAGMA user_version = ", currentPushDatabaseVersion)))
RELEASE_LOG_ERROR(Push, "Error setting user version for PushDatabase at path %s: %d", path.utf8().data(), db->lastError());
transaction.commit();
}
return db;
}
static std::unique_ptr<SQLiteDatabase> openAndMigrateDatabase(const String& path)
{
ASSERT(!RunLoop::isMain());
auto result = openAndMigrateDatabaseImpl(path);
if (!result && result.error() == ShouldDeleteAndRetry::Yes) {
if (path == SQLiteDatabase::inMemoryPath() || !SQLiteFileSystem::deleteDatabaseFile(path)) {
RELEASE_LOG(Push, "Failed to delete PushDatabase at path %s; bailing on recreating from scratch", path.utf8().data());
return nullptr;
}
RELEASE_LOG(Push, "Deleted PushDatabase at path %s and recreating from scratch", path.utf8().data());
result = openAndMigrateDatabaseImpl(path);
}
if (!result)
return nullptr;
auto database = WTFMove(*result);
return database.moveToUniquePtr();
}
void PushDatabase::create(const String& path, CreationHandler&& completionHandler)
{
ASSERT(RunLoop::isMain());
auto queue = WorkQueue::create("PushDatabase I/O Thread");
queue->dispatch([queue, path = crossThreadCopy(path), completionHandler = WTFMove(completionHandler)]() mutable {
auto database = openAndMigrateDatabase(path);
WorkQueue::main().dispatch([queue = WTFMove(queue), database = WTFMove(database), completionHandler = WTFMove(completionHandler)]() mutable {
if (!database) {
completionHandler(nullptr);
return;
}
completionHandler(std::unique_ptr<PushDatabase>(new PushDatabase(WTFMove(queue), makeUniqueRefFromNonNullUniquePtr(WTFMove(database)))));
});
});
}
PushDatabase::PushDatabase(Ref<WorkQueue>&& queue, UniqueRef<SQLiteDatabase>&& db)
: m_queue(WTFMove(queue))
, m_db(WTFMove(db))
{
}
PushDatabase::~PushDatabase()
{
// In practice we aren't actually expecting this to run, since a NeverDestroyed<WebPushDaemon> instance
// holds on to this object.
//
// If we intend to delete this object for real, we should probably make this object refcounted and make
// the blocks on the queue protect the database object rather than using dispatchSync.
ASSERT(RunLoop::isMain());
// Flush any outstanding requests.
m_queue->dispatchSync([]() { });
// Finalize member variables on the queue, since they were are only meant to be used on the queue.
m_queue->dispatchSync([db = WTFMove(m_db), statements = WTFMove(m_statements)]() mutable {
statements.clear();
db->close();
});
}
void PushDatabase::dispatchOnWorkQueue(Function<void()>&& function)
{
RELEASE_ASSERT(RunLoop::isMain());
m_queue->dispatch(WTFMove(function));
}
SQLiteStatementAutoResetScope PushDatabase::cachedStatementOnQueue(ASCIILiteral query)
{
ASSERT(!RunLoop::isMain());
auto it = m_statements.find(query);
if (it != m_statements.end())
return SQLiteStatementAutoResetScope(it->value.ptr());
auto result = m_db->prepareHeapStatement(query);
if (!result) {
PUSHDB_RELEASE_LOG_ERROR("Failed with %d preparing statement: %" PUBLIC_LOG_STRING, result.error(), query.characters());
return SQLiteStatementAutoResetScope(nullptr);
}
auto ref = WTFMove(*result);
auto statement = ref.ptr();
m_statements.add(query, WTFMove(ref));
return SQLiteStatementAutoResetScope(statement);
}
static int bindExpirationTime(SQLiteStatement* sql, int index, std::optional<EpochTimeStamp> timestamp)
{
return timestamp ? sql->bindInt64(index, convertEpochTimeStampToSeconds(*timestamp)) : sql->bindNull(index);
}
static std::optional<EpochTimeStamp> expirationTimeFromValue(SQLValue value)
{
if (std::holds_alternative<double>(value))
return convertSecondsToEpochTimeStamp(std::get<double>(value));
return std::nullopt;
}
template <class T, class U>
static void completeOnMainQueue(CompletionHandler<void(T)>&& completionHandler, U&& result)
{
ASSERT(!RunLoop::isMain());
WorkQueue::main().dispatch([completionHandler = WTFMove(completionHandler), result = crossThreadCopy(std::forward<U>(result))]() mutable {
completionHandler(WTFMove(result));
});
}
void PushDatabase::updatePublicToken(Span<const uint8_t> publicToken, CompletionHandler<void(PublicTokenChanged)>&& completionHandler)
{
dispatchOnWorkQueue([this, newPublicToken = Vector<uint8_t> { publicToken }, completionHandler = WTFMove(completionHandler)]() mutable {
SQLiteTransaction transaction(m_db);
transaction.begin();
auto result = PublicTokenChanged::No;
Vector<uint8_t> currentPublicToken;
auto scope = makeScopeExit([&completionHandler, &result] {
completeOnMainQueue(WTFMove(completionHandler), result);
});
{
auto sql = cachedStatementOnQueue("SELECT value FROM Metadata WHERE key = ?"_s);
if (!sql || sql->bindText(1, publicTokenKey) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() == SQLITE_ROW)
currentPublicToken = sql->columnBlob(0);
}
if (currentPublicToken == newPublicToken)
return;
{
auto sql = cachedStatementOnQueue("REPLACE INTO Metadata(key, value) VALUES(?, ?)"_s);
if (!sql
|| sql->bindText(1, publicTokenKey) != SQLITE_OK
|| sql->bindBlob(2, newPublicToken) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(Push, "Failed to save new public token: %d", m_db->lastError());
return;
}
}
// If we are updating an old version of the database where currentPublicToken doesn't exist, just
// save the initial publicToken without deleting all subscriptions and notifying the caller that
// the token changed.
if (!currentPublicToken.isEmpty()) {
auto deleteSubscriptionSets = cachedStatementOnQueue("DELETE FROM SubscriptionSets"_s);
auto deleteSubscriptions = cachedStatementOnQueue("DELETE FROM Subscriptions"_s);
if (!deleteSubscriptionSets || !deleteSubscriptions) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (deleteSubscriptionSets->step() != SQLITE_DONE || deleteSubscriptions->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(Push, "Failed to delete subscriptions: %d", m_db->lastError());
return;
}
result = PublicTokenChanged::Yes;
}
scope.release();
transaction.commit();
completeOnMainQueue(WTFMove(completionHandler), result);
});
}
void PushDatabase::getPublicToken(CompletionHandler<void(Vector<uint8_t>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, completionHandler = WTFMove(completionHandler)]() mutable {
SQLiteTransaction transaction(m_db);
transaction.begin();
auto sql = cachedStatementOnQueue("SELECT value FROM Metadata WHERE key = ?"_s);
if (!sql || sql->bindText(1, publicTokenKey) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), Vector<uint8_t> { });
return;
}
Vector<uint8_t> result;
if (sql->step() == SQLITE_ROW)
result = sql->columnBlob(0);
transaction.commit();
completeOnMainQueue(WTFMove(completionHandler), WTFMove(result));
});
}
void PushDatabase::insertRecord(const PushRecord& record, CompletionHandler<void(std::optional<PushRecord>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, record = crossThreadCopy(record), completionHandler = WTFMove(completionHandler)]() mutable {
SQLiteTransaction transaction(m_db);
transaction.begin();
int64_t subscriptionSetID = 0;
{
auto sql = cachedStatementOnQueue("SELECT rowID FROM SubscriptionSets WHERE bundleID = ? AND securityOrigin = ?"_s);
if (!sql
|| sql->bindText(1, record.bundleID) != SQLITE_OK
|| sql->bindText(2, record.securityOrigin) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
if (sql->step() == SQLITE_ROW)
subscriptionSetID = sql->columnInt64(0);
}
if (!subscriptionSetID) {
auto sql = cachedStatementOnQueue("INSERT INTO SubscriptionSets VALUES(NULL, ?, ?, ?, 0)"_s);
if (!sql
|| sql->bindInt64(1, time(nullptr)) != SQLITE_OK
|| sql->bindText(2, record.bundleID) != SQLITE_OK
|| sql->bindText(3, record.securityOrigin) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
if (sql->step() != SQLITE_DONE) {
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
subscriptionSetID = m_db->lastInsertRowID();
}
{
auto sql = cachedStatementOnQueue("INSERT INTO Subscriptions VALUES(NULL, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"_s);
if (!sql
|| sql->bindInt64(1, time(nullptr)) != SQLITE_OK
|| sql->bindInt64(2, subscriptionSetID) != SQLITE_OK
|| sql->bindText(3, record.scope) != SQLITE_OK
|| sql->bindText(4, record.endpoint) != SQLITE_OK
|| sql->bindText(5, record.topic) != SQLITE_OK
|| sql->bindBlob(6, record.serverVAPIDPublicKey) != SQLITE_OK
|| sql->bindBlob(7, record.clientPublicKey) != SQLITE_OK
|| sql->bindBlob(8, record.clientPrivateKey) != SQLITE_OK
|| sql->bindBlob(9, record.sharedAuthSecret) != SQLITE_OK
|| bindExpirationTime(sql.get(), 10, record.expirationTime) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
if (sql->step() != SQLITE_DONE) {
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
record.identifier = makeObjectIdentifier<PushSubscriptionIdentifierType>(m_db->lastInsertRowID());
}
transaction.commit();
completeOnMainQueue(WTFMove(completionHandler), WTFMove(record));
});
}
void PushDatabase::removeRecordByIdentifier(PushSubscriptionIdentifier identifier, CompletionHandler<void(bool)>&& completionHandler)
{
dispatchOnWorkQueue([this, rowIdentifier = identifier.toUInt64(), completionHandler = WTFMove(completionHandler)]() mutable {
SQLiteTransaction transaction(m_db);
transaction.begin();
bool isLastSubscriptionInSet = false;
int64_t subscriptionSetID = 0;
{
auto sql = cachedStatementOnQueue("SELECT subscriptionSetID FROM Subscriptions WHERE rowid = ?"_s);
if (!sql || sql->bindInt64(1, rowIdentifier) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
if (sql->step() != SQLITE_ROW) {
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
subscriptionSetID = sql->columnInt64(0);
}
{
// TODO: on SQLite >3.35.0, we could use RETURNING to avoid the SELECT above.
auto sql = cachedStatementOnQueue("DELETE FROM Subscriptions WHERE rowid = ?"_s);
if (!sql || sql->bindInt64(1, rowIdentifier) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
if (sql->step() != SQLITE_DONE) {
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
}
{
// Check if this was the last subscription in the subscription set.
auto sql = cachedStatementOnQueue("SELECT rowid FROM Subscriptions WHERE subscriptionSetID = ?"_s);
if (!sql || sql->bindInt64(1, subscriptionSetID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
isLastSubscriptionInSet = (sql->step() == SQLITE_DONE);
}
if (isLastSubscriptionInSet) {
// Delete the entire subscription set if it is no longer associated with any subscriptions.
auto sql = cachedStatementOnQueue("DELETE FROM SubscriptionSets WHERE rowid = ?"_s);
if (!sql || sql->bindInt64(1, subscriptionSetID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
if (sql->step() != SQLITE_DONE) {
completeOnMainQueue(WTFMove(completionHandler), false);
return;
}
}
transaction.commit();
completeOnMainQueue(WTFMove(completionHandler), true);
});
}
static PushRecord makePushRecordFromRow(SQLiteStatementAutoResetScope& sql, int columnIndex)
{
PushRecord record;
record.identifier = makeObjectIdentifier<PushSubscriptionIdentifierType>(sql->columnInt64(columnIndex++));
record.bundleID = sql->columnText(columnIndex++);
record.securityOrigin = sql->columnText(columnIndex++);
record.scope = sql->columnText(columnIndex++);
record.endpoint = sql->columnText(columnIndex++);
record.topic = sql->columnText(columnIndex++);
record.serverVAPIDPublicKey = sql->columnBlob(columnIndex++);
record.clientPublicKey = sql->columnBlob(columnIndex++);
record.clientPrivateKey = sql->columnBlob(columnIndex++);
record.sharedAuthSecret = sql->columnBlob(columnIndex++);
record.expirationTime = expirationTimeFromValue(sql->columnValue(columnIndex++));
return record;
}
void PushDatabase::getRecordByTopic(const String& topic, CompletionHandler<void(std::optional<PushRecord>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, topic = crossThreadCopy(topic), completionHandler = WTFMove(completionHandler)]() mutable {
// Force SQLite to consult the Subscriptions(scope) index first via CROSS JOIN.
auto sql = cachedStatementOnQueue(
"SELECT " kPushRecordColumns
"FROM Subscriptions sub "
"CROSS JOIN SubscriptionSets ss "
"ON sub.subscriptionSetID = ss.rowid "
"WHERE sub.topic = ?"_s);
if (!sql || sql->bindText(1, topic) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
if (sql->step() != SQLITE_ROW) {
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
completeOnMainQueue(WTFMove(completionHandler), makePushRecordFromRow(sql, 0));
});
}
void PushDatabase::getRecordByBundleIdentifierAndScope(const String& bundleID, const String& scope, CompletionHandler<void(std::optional<PushRecord>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, bundleID = crossThreadCopy(bundleID), scope = crossThreadCopy(scope), completionHandler = WTFMove(completionHandler)]() mutable {
// Force SQLite to consult the Subscriptions(scope) index first via CROSS JOIN.
auto sql = cachedStatementOnQueue(
"SELECT " kPushRecordColumns
"FROM Subscriptions sub "
"CROSS JOIN SubscriptionSets ss "
"ON sub.subscriptionSetID = ss.rowid "
"WHERE sub.scope = ? AND ss.bundleID = ?"_s);
if (!sql || sql->bindText(1, scope) != SQLITE_OK || sql->bindText(2, bundleID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
if (sql->step() != SQLITE_ROW) {
completeOnMainQueue(WTFMove(completionHandler), std::optional<PushRecord> { });
return;
}
completeOnMainQueue(WTFMove(completionHandler), makePushRecordFromRow(sql, 0));
});
}
void PushDatabase::getIdentifiers(CompletionHandler<void(HashSet<PushSubscriptionIdentifier>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, completionHandler = WTFMove(completionHandler)]() mutable {
HashSet<PushSubscriptionIdentifier> result;
auto sql = cachedStatementOnQueue("SELECT rowid FROM Subscriptions"_s);
while (sql && sql->step() == SQLITE_ROW)
result.add(makeObjectIdentifier<PushSubscriptionIdentifierType>(sql->columnInt64(0)));
completeOnMainQueue(WTFMove(completionHandler), WTFMove(result));
});
}
void PushDatabase::getTopics(CompletionHandler<void(Vector<String>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, completionHandler = WTFMove(completionHandler)]() mutable {
Vector<String> topics;
auto sql = cachedStatementOnQueue("SELECT topic FROM Subscriptions"_s);
if (!sql) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
completeOnMainQueue(WTFMove(completionHandler), Vector<String> { });
return;
}
while (sql->step() == SQLITE_ROW)
topics.append(sql->columnText(0));
completeOnMainQueue(WTFMove(completionHandler), WTFMove(topics));
});
}
void PushDatabase::incrementSilentPushCount(const String& bundleID, const String& securityOrigin, CompletionHandler<void(unsigned)>&& completionHandler)
{
dispatchOnWorkQueue([this, bundleID = crossThreadCopy(bundleID), securityOrigin = crossThreadCopy(securityOrigin), completionHandler = WTFMove(completionHandler)]() mutable {
auto scope = makeScopeExit([&completionHandler] {
completeOnMainQueue(WTFMove(completionHandler), 0u);
});
int silentPushCount = 0;
SQLiteTransaction transaction(m_db);
transaction.begin();
{
auto sql = cachedStatementOnQueue("UPDATE SubscriptionSets SET silentPushCount = silentPushCount + 1 WHERE bundleID = ? AND securityOrigin = ?"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK || sql->bindText(2, securityOrigin) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE)
return;
}
{
auto sql = cachedStatementOnQueue("SELECT silentPushCount FROM SubscriptionSets WHERE bundleID = ? AND securityOrigin = ?"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK || sql->bindText(2, securityOrigin) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() == SQLITE_ROW)
silentPushCount = sql->columnInt(0);
}
transaction.commit();
scope.release();
completeOnMainQueue(WTFMove(completionHandler), silentPushCount);
});
}
void PushDatabase::removeRecordsByBundleIdentifier(const String& bundleID, CompletionHandler<void(Vector<RemovedPushRecord>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, bundleID = crossThreadCopy(bundleID), completionHandler = WTFMove(completionHandler)]() mutable {
auto scope = makeScopeExit([&completionHandler] {
completeOnMainQueue(WTFMove(completionHandler), Vector<RemovedPushRecord> { });
});
Vector<RemovedPushRecord> removedPushRecords;
SQLiteTransaction transaction(m_db);
transaction.begin();
{
auto sql = cachedStatementOnQueue(
"SELECT sub.subscriptionSetID, sub.rowid, sub.topic, sub.serverVAPIDPublicKey "
"FROM SubscriptionSets ss "
"JOIN Subscriptions sub "
"ON ss.rowid = sub.subscriptionSetID "
"WHERE ss.bundleID = ?"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
while (sql->step() == SQLITE_ROW) {
auto identifier = makeObjectIdentifier<PushSubscriptionIdentifierType>(sql->columnInt(1));
auto topic = sql->columnText(2);
auto serverVAPIDPublicKey = sql->columnBlob(3);
removedPushRecords.append({ identifier, WTFMove(topic), WTFMove(serverVAPIDPublicKey) });
}
}
{
auto sql = cachedStatementOnQueue("DELETE FROM Subscriptions WHERE subscriptionSetID IN (SELECT rowid FROM SubscriptionSets WHERE bundleID = ?)"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE)
return;
}
{
auto sql = cachedStatementOnQueue("DELETE FROM SubscriptionSets WHERE bundleID = ?"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE)
return;
}
transaction.commit();
scope.release();
completeOnMainQueue(WTFMove(completionHandler), WTFMove(removedPushRecords));
});
}
void PushDatabase::removeRecordsByBundleIdentifierAndSecurityOrigin(const String& bundleID, const String& securityOrigin, CompletionHandler<void(Vector<RemovedPushRecord>&&)>&& completionHandler)
{
dispatchOnWorkQueue([this, bundleID = crossThreadCopy(bundleID), securityOrigin = crossThreadCopy(securityOrigin), completionHandler = WTFMove(completionHandler)]() mutable {
auto scope = makeScopeExit([&completionHandler] {
completeOnMainQueue(WTFMove(completionHandler), Vector<RemovedPushRecord> { });
});
Vector<RemovedPushRecord> removedPushRecords;
SQLiteTransaction transaction(m_db);
transaction.begin();
int64_t subscriptionSetID = 0;
{
auto sql = cachedStatementOnQueue(
"SELECT sub.subscriptionSetID, sub.rowid, sub.topic, sub.serverVAPIDPublicKey "
"FROM SubscriptionSets ss "
"JOIN Subscriptions sub "
"ON ss.rowid = sub.subscriptionSetID "
"WHERE ss.bundleID = ? AND ss.securityOrigin = ?"_s);
if (!sql || sql->bindText(1, bundleID) != SQLITE_OK || sql->bindText(2, securityOrigin) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
while (sql->step() == SQLITE_ROW) {
subscriptionSetID = sql->columnInt(0);
auto identifier = makeObjectIdentifier<PushSubscriptionIdentifierType>(sql->columnInt(1));
auto topic = sql->columnText(2);
auto serverVAPIDPublicKey = sql->columnBlob(3);
removedPushRecords.append({ identifier, WTFMove(topic), WTFMove(serverVAPIDPublicKey) });
}
}
{
auto sql = cachedStatementOnQueue("DELETE FROM Subscriptions WHERE subscriptionSetID = ?"_s);
if (!sql || sql->bindInt(1, subscriptionSetID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE)
return;
}
{
auto sql = cachedStatementOnQueue("DELETE FROM SubscriptionSets WHERE rowid = ?"_s);
if (!sql || sql->bindInt(1, subscriptionSetID) != SQLITE_OK) {
PUSHDB_RELEASE_LOG_BIND_ERROR();
return;
}
if (sql->step() != SQLITE_DONE)
return;
}
transaction.commit();
scope.release();
completeOnMainQueue(WTFMove(completionHandler), WTFMove(removedPushRecords));
});
}
} // namespace WebCore
#endif // ENABLE(SERVICE_WORKER)