blob: 1486d52296f6751f310c798166bf7edbed2ab432 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "DatabaseUtilities.h"
#include "Logging.h"
#include "PrivateClickMeasurementManager.h"
#include <WebCore/PrivateClickMeasurement.h>
#include <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteStatementAutoResetScope.h>
#include <wtf/FileSystem.h>
#include <wtf/RunLoop.h>
namespace WebKit {
DatabaseUtilities::DatabaseUtilities(String&& storageFilePath)
: m_storageFilePath(WTFMove(storageFilePath))
, m_transaction(m_database)
{
ASSERT(!RunLoop::isMain());
}
DatabaseUtilities::~DatabaseUtilities()
{
ASSERT(!RunLoop::isMain());
}
WebCore::SQLiteStatementAutoResetScope DatabaseUtilities::scopedStatement(std::unique_ptr<WebCore::SQLiteStatement>& statement, ASCIILiteral query, ASCIILiteral logString) const
{
ASSERT(!RunLoop::isMain());
if (!statement) {
auto statementOrError = m_database.prepareHeapStatement(query);
if (!statementOrError) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::%s failed to prepare statement, error message: %" PUBLIC_LOG_STRING, this, logString.characters(), m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return WebCore::SQLiteStatementAutoResetScope { };
}
statement = statementOrError.value().moveToUniquePtr();
ASSERT(m_database.isOpen());
}
return WebCore::SQLiteStatementAutoResetScope { statement.get() };
}
ScopeExit<Function<void()>> DatabaseUtilities::beginTransactionIfNecessary()
{
ASSERT(!RunLoop::isMain());
if (m_transaction.inProgress())
return makeScopeExit(Function<void()> { [] { } });
m_transaction.begin();
return makeScopeExit(Function<void()> { [this] {
m_transaction.commit();
} });
}
auto DatabaseUtilities::openDatabaseAndCreateSchemaIfNecessary() -> CreatedNewFile
{
ASSERT(!RunLoop::isMain());
CreatedNewFile createdNewFile = CreatedNewFile::No;
if (!FileSystem::fileExists(m_storageFilePath)) {
if (!FileSystem::makeAllDirectories(FileSystem::parentPath(m_storageFilePath))) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::open failed, error message: Failed to create directory database path: %" PUBLIC_LOG_STRING, this, m_storageFilePath.utf8().data());
ASSERT_NOT_REACHED();
return createdNewFile;
}
createdNewFile = CreatedNewFile::Yes;
}
if (!m_database.open(m_storageFilePath)) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::open failed, error message: %" PUBLIC_LOG_STRING ", database path: %" PUBLIC_LOG_STRING, this, m_database.lastErrorMsg(), m_storageFilePath.utf8().data());
ASSERT_NOT_REACHED();
return createdNewFile;
}
// Since we are using a workerQueue, the sequential dispatch blocks may be called by different threads.
m_database.disableThreadingChecks();
auto setBusyTimeout = m_database.prepareStatement("PRAGMA busy_timeout = 5000"_s);
if (!setBusyTimeout || setBusyTimeout->step() != SQLITE_ROW) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::setBusyTimeout failed, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
}
if (createdNewFile == CreatedNewFile::Yes) {
if (!createSchema()) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::createSchema failed, error message: %" PUBLIC_LOG_STRING ", database path: %" PUBLIC_LOG_STRING, this, m_database.lastErrorMsg(), m_storageFilePath.utf8().data());
ASSERT_NOT_REACHED();
}
}
return createdNewFile;
}
void DatabaseUtilities::enableForeignKeys()
{
auto enableForeignKeys = m_database.prepareStatement("PRAGMA foreign_keys = ON"_s);
if (!enableForeignKeys || enableForeignKeys->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::enableForeignKeys failed, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
}
}
void DatabaseUtilities::close()
{
ASSERT(!RunLoop::isMain());
destroyStatements();
if (m_database.isOpen())
m_database.close();
}
void DatabaseUtilities::interrupt()
{
ASSERT(!RunLoop::isMain());
if (m_database.isOpen())
m_database.interrupt();
}
WebCore::PrivateClickMeasurement DatabaseUtilities::buildPrivateClickMeasurementFromDatabase(WebCore::SQLiteStatement& statement, PrivateClickMeasurementAttributionType attributionType) const
{
ASSERT(!RunLoop::isMain());
auto sourceSiteDomain = getDomainStringFromDomainID(statement.columnInt(0));
auto destinationSiteDomain = getDomainStringFromDomainID(statement.columnInt(1));
auto sourceID = statement.columnInt(2);
auto timeOfAdClick = attributionType == PrivateClickMeasurementAttributionType::Attributed ? statement.columnDouble(5) : statement.columnDouble(3);
auto token = attributionType == PrivateClickMeasurementAttributionType::Attributed ? statement.columnText(7) : statement.columnText(4);
auto signature = attributionType == PrivateClickMeasurementAttributionType::Attributed ? statement.columnText(8) : statement.columnText(5);
auto keyID = attributionType == PrivateClickMeasurementAttributionType::Attributed ? statement.columnText(9) : statement.columnText(6);
auto bundleID = attributionType == PrivateClickMeasurementAttributionType::Attributed ? statement.columnText(11) : statement.columnText(7);
// Safari was the only application that used PCM when it was stored with ResourceLoadStatistics.
#if PLATFORM(MAC)
constexpr auto safariBundleID = "com.apple.Safari"_s;
#else
constexpr auto safariBundleID = "com.apple.mobilesafari"_s;
#endif
if (bundleID.isEmpty())
bundleID = safariBundleID;
WebCore::PrivateClickMeasurement attribution(WebCore::PrivateClickMeasurement::SourceID(sourceID), WebCore::PrivateClickMeasurement::SourceSite(WebCore::RegistrableDomain::uncheckedCreateFromRegistrableDomainString(sourceSiteDomain)), WebCore::PrivateClickMeasurement::AttributionDestinationSite(WebCore::RegistrableDomain::uncheckedCreateFromRegistrableDomainString(destinationSiteDomain)), bundleID, WallTime::fromRawSeconds(timeOfAdClick), WebCore::PrivateClickMeasurement::AttributionEphemeral::No);
if (attributionType == PrivateClickMeasurementAttributionType::Attributed) {
auto attributionTriggerData = statement.columnInt(3);
auto priority = statement.columnInt(4);
auto sourceEarliestTimeToSendValue = statement.columnDouble(6);
auto destinationEarliestTimeToSendValue = statement.columnDouble(10);
if (attributionTriggerData != -1)
attribution.setAttribution(WebCore::PrivateClickMeasurement::AttributionTriggerData { static_cast<uint8_t>(attributionTriggerData), WebCore::PrivateClickMeasurement::Priority(priority) });
std::optional<WallTime> sourceEarliestTimeToSend;
std::optional<WallTime> destinationEarliestTimeToSend;
// A value of 0.0 indicates that the report has been sent to the respective site.
if (sourceEarliestTimeToSendValue > 0.0)
sourceEarliestTimeToSend = WallTime::fromRawSeconds(sourceEarliestTimeToSendValue);
if (destinationEarliestTimeToSendValue > 0.0)
destinationEarliestTimeToSend = WallTime::fromRawSeconds(destinationEarliestTimeToSendValue);
attribution.setTimesToSend({ sourceEarliestTimeToSend, destinationEarliestTimeToSend });
}
attribution.setSourceSecretToken({ token, signature, keyID });
return attribution;
}
String DatabaseUtilities::stripIndexQueryToMatchStoredValue(const char* originalQuery)
{
return String(originalQuery).replace("CREATE UNIQUE INDEX IF NOT EXISTS", "CREATE UNIQUE INDEX");
}
TableAndIndexPair DatabaseUtilities::currentTableAndIndexQueries(const String& tableName)
{
auto getTableStatement = m_database.prepareStatement("SELECT sql FROM sqlite_master WHERE tbl_name=? AND type = 'table'"_s);
if (!getTableStatement) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::currentTableAndIndexQueries Unable to prepare statement to fetch schema for the table, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return { };
}
if (getTableStatement->bindText(1, tableName) != SQLITE_OK) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::currentTableAndIndexQueries Unable to bind statement to fetch schema for the table, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return { };
}
if (getTableStatement->step() != SQLITE_ROW) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::currentTableAndIndexQueries error executing statement to fetch table schema, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return { };
}
String createTableQuery = getTableStatement->columnText(0);
auto getIndexStatement = m_database.prepareStatement("SELECT sql FROM sqlite_master WHERE tbl_name=? AND type = 'index'"_s);
if (!getIndexStatement) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::currentTableAndIndexQueries Unable to prepare statement to fetch index for the table, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return { };
}
if (getIndexStatement->bindText(1, tableName) != SQLITE_OK) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::currentTableAndIndexQueries Unable to bind statement to fetch index for the table, error message: %" PRIVATE_LOG_STRING, this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return { };
}
std::optional<String> index;
if (getIndexStatement->step() == SQLITE_ROW) {
auto rawIndex = String(getIndexStatement->columnText(0));
if (!rawIndex.isEmpty())
index = rawIndex;
}
return std::make_pair<String, std::optional<String>>(WTFMove(createTableQuery), WTFMove(index));
}
static Expected<WebCore::SQLiteStatement, int> insertDistinctValuesInTableStatement(WebCore::SQLiteDatabase& database, const String& table)
{
if (table == "SubframeUnderTopFrameDomains")
return database.prepareStatement("INSERT INTO SubframeUnderTopFrameDomains SELECT subFrameDomainID, MAX(lastUpdated), topFrameDomainID FROM _SubframeUnderTopFrameDomains GROUP BY subFrameDomainID, topFrameDomainID"_s);
if (table == "SubresourceUnderTopFrameDomains")
return database.prepareStatement("INSERT INTO SubresourceUnderTopFrameDomains SELECT subresourceDomainID, MAX(lastUpdated), topFrameDomainID FROM _SubresourceUnderTopFrameDomains GROUP BY subresourceDomainID, topFrameDomainID"_s);
if (table == "SubresourceUniqueRedirectsTo")
return database.prepareStatement("INSERT INTO SubresourceUniqueRedirectsTo SELECT subresourceDomainID, MAX(lastUpdated), toDomainID FROM _SubresourceUniqueRedirectsTo GROUP BY subresourceDomainID, toDomainID"_s);
if (table == "TopFrameLinkDecorationsFrom")
return database.prepareStatement("INSERT INTO TopFrameLinkDecorationsFrom SELECT toDomainID, MAX(lastUpdated), fromDomainID FROM _TopFrameLinkDecorationsFrom GROUP BY toDomainID, fromDomainID"_s);
return database.prepareStatementSlow(makeString("INSERT INTO ", table, " SELECT DISTINCT * FROM _", table));
}
void DatabaseUtilities::migrateDataToNewTablesIfNecessary()
{
if (!needsUpdatedSchema())
return;
auto transactionScope = beginTransactionIfNecessary();
for (auto& table : expectedTableAndIndexQueries().keys()) {
auto alterTable = m_database.prepareStatementSlow(makeString("ALTER TABLE ", table, " RENAME TO _", table));
if (!alterTable || alterTable->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::migrateDataToNewTablesIfNecessary failed to rename table, error message: %s", this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return;
}
}
if (!createSchema()) {
ASSERT_NOT_REACHED();
return;
}
// Maintain the order of tables to make sure the ObservedDomains table is created first. Other tables have foreign key constraints referencing it.
for (auto& table : sortedTables()) {
auto migrateTableData = insertDistinctValuesInTableStatement(m_database, table);
if (!migrateTableData || migrateTableData->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::migrateDataToNewTablesIfNecessary (table %s) failed to migrate schema, error message: %s", this, table.characters(), m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return;
}
}
// Drop all tables at the end to avoid trashing data that references data in other tables.
for (auto& table : sortedTables()) {
auto dropTableQuery = m_database.prepareStatementSlow(makeString("DROP TABLE _", table));
if (!dropTableQuery || dropTableQuery->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::migrateDataToNewTablesIfNecessary failed to drop temporary tables, error message: %s", this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
return;
}
}
if (!createUniqueIndices()) {
RELEASE_LOG_ERROR(PrivateClickMeasurement, "%p - DatabaseUtilities::migrateDataToNewTablesIfNecessary failed to create unique indices, error message: %s", this, m_database.lastErrorMsg());
ASSERT_NOT_REACHED();
}
}
} // namespace WebKit