gecko-dev/dom/storage/StorageDBUpdater.cpp

470 lines
15 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "LocalStorageManager.h"
#include "StorageUtils.h"
#include "mozIStorageBindingParams.h"
#include "mozIStorageValueArray.h"
#include "mozIStorageFunction.h"
#include "mozilla/BasePrincipal.h"
#include "nsVariant.h"
#include "mozilla/Tokenizer.h"
// Current version of the database schema
#define CURRENT_SCHEMA_VERSION 2
namespace mozilla {
namespace dom {
using namespace StorageUtils;
namespace {
class nsReverseStringSQLFunction final : public mozIStorageFunction {
~nsReverseStringSQLFunction() = default;
NS_DECL_ISUPPORTS
NS_DECL_MOZISTORAGEFUNCTION
};
NS_IMPL_ISUPPORTS(nsReverseStringSQLFunction, mozIStorageFunction)
NS_IMETHODIMP
nsReverseStringSQLFunction::OnFunctionCall(
mozIStorageValueArray* aFunctionArguments, nsIVariant** aResult) {
nsresult rv;
nsAutoCString stringToReverse;
rv = aFunctionArguments->GetUTF8String(0, stringToReverse);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString result;
ReverseString(stringToReverse, result);
RefPtr<nsVariant> outVar(new nsVariant());
rv = outVar->SetAsAUTF8String(result);
NS_ENSURE_SUCCESS(rv, rv);
outVar.forget(aResult);
return NS_OK;
}
// "scope" to "origin attributes suffix" and "origin key" convertor
class ExtractOriginData : protected mozilla::Tokenizer {
public:
ExtractOriginData(const nsACString& scope, nsACString& suffix,
nsACString& origin)
: mozilla::Tokenizer(scope) {
using mozilla::OriginAttributes;
// Parse optional appId:isInIsolatedMozBrowserElement: string, in case
// we don't find it, the scope is our new origin key and suffix
// is empty.
suffix.Truncate();
origin.Assign(scope);
// Bail out if it isn't appId.
// AppId doesn't exist any more but we could have old storage data...
uint32_t appId;
if (!ReadInteger(&appId)) {
return;
}
// Should be followed by a colon.
if (!CheckChar(':')) {
return;
}
// Bail out if it isn't 'isolatedBrowserFlag'.
nsDependentCSubstring isolatedBrowserFlag;
if (!ReadWord(isolatedBrowserFlag)) {
return;
}
bool inIsolatedMozBrowser = isolatedBrowserFlag == "t";
bool notInIsolatedBrowser = isolatedBrowserFlag == "f";
if (!inIsolatedMozBrowser && !notInIsolatedBrowser) {
return;
}
// Should be followed by a colon.
if (!CheckChar(':')) {
return;
}
// OK, we have found appId and inIsolatedMozBrowser flag, create the suffix
// from it and take the rest as the origin key.
// If the profile went through schema 1 -> schema 0 -> schema 1 switching
// we may have stored the full attributes origin suffix when there were
// more than just appId and inIsolatedMozBrowser set on storage principal's
// OriginAttributes.
//
// To preserve full uniqueness we store this suffix to the scope key.
// Schema 0 code will just ignore it while keeping the scoping unique.
//
// The whole scope string is in one of the following forms (when we are
// here):
//
// "1001:f:^appId=1001&inBrowser=false&addonId=101:gro.allizom.rxd.:https:443"
// "1001:f:gro.allizom.rxd.:https:443"
// |
// +- the parser cursor position.
//
// If there is '^', the full origin attributes suffix follows. We search
// for ':' since it is the delimiter used in the scope string and is never
// contained in the origin attributes suffix. Remaining string after
// the comma is the reversed-domain+schema+port tuple.
Record();
if (CheckChar('^')) {
Token t;
while (Next(t)) {
if (t.Equals(Token::Char(':'))) {
Claim(suffix);
break;
}
}
} else {
OriginAttributes attrs(inIsolatedMozBrowser);
attrs.CreateSuffix(suffix);
}
// Consume the rest of the input as "origin".
origin.Assign(Substring(mCursor, mEnd));
}
};
class GetOriginParticular final : public mozIStorageFunction {
public:
enum EParticular { ORIGIN_ATTRIBUTES_SUFFIX, ORIGIN_KEY };
explicit GetOriginParticular(EParticular aParticular)
: mParticular(aParticular) {}
private:
GetOriginParticular() = delete;
~GetOriginParticular() = default;
EParticular mParticular;
NS_DECL_ISUPPORTS
NS_DECL_MOZISTORAGEFUNCTION
};
NS_IMPL_ISUPPORTS(GetOriginParticular, mozIStorageFunction)
NS_IMETHODIMP
GetOriginParticular::OnFunctionCall(mozIStorageValueArray* aFunctionArguments,
nsIVariant** aResult) {
nsresult rv;
nsAutoCString scope;
rv = aFunctionArguments->GetUTF8String(0, scope);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString suffix, origin;
ExtractOriginData extractor(scope, suffix, origin);
nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
switch (mParticular) {
case EParticular::ORIGIN_ATTRIBUTES_SUFFIX:
rv = outVar->SetAsAUTF8String(suffix);
break;
case EParticular::ORIGIN_KEY:
rv = outVar->SetAsAUTF8String(origin);
break;
}
NS_ENSURE_SUCCESS(rv, rv);
outVar.forget(aResult);
return NS_OK;
}
class StripOriginAddonId final : public mozIStorageFunction {
public:
explicit StripOriginAddonId() = default;
private:
~StripOriginAddonId() = default;
NS_DECL_ISUPPORTS
NS_DECL_MOZISTORAGEFUNCTION
};
NS_IMPL_ISUPPORTS(StripOriginAddonId, mozIStorageFunction)
NS_IMETHODIMP
StripOriginAddonId::OnFunctionCall(mozIStorageValueArray* aFunctionArguments,
nsIVariant** aResult) {
nsresult rv;
nsAutoCString suffix;
rv = aFunctionArguments->GetUTF8String(0, suffix);
NS_ENSURE_SUCCESS(rv, rv);
// Deserialize and re-serialize to automatically drop any obsolete origin
// attributes.
OriginAttributes oa;
bool ok = oa.PopulateFromSuffix(suffix);
NS_ENSURE_TRUE(ok, NS_ERROR_FAILURE);
nsAutoCString newSuffix;
oa.CreateSuffix(newSuffix);
nsCOMPtr<nsIWritableVariant> outVar = new nsVariant();
rv = outVar->SetAsAUTF8String(newSuffix);
NS_ENSURE_SUCCESS(rv, rv);
outVar.forget(aResult);
return NS_OK;
}
} // namespace
namespace StorageDBUpdater {
nsresult CreateSchema1Tables(mozIStorageConnection* aWorkerConnection) {
nsresult rv;
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"CREATE TABLE IF NOT EXISTS webappsstore2 ("
"originAttributes TEXT, "
"originKey TEXT, "
"scope TEXT, " // Only for schema0 downgrade compatibility
"key TEXT, "
"value TEXT)"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(
nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS origin_key_index"
" ON webappsstore2(originAttributes, originKey, key)"));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Update(mozIStorageConnection* aWorkerConnection) {
nsresult rv;
mozStorageTransaction transaction(aWorkerConnection, false);
bool doVacuum = false;
int32_t schemaVer;
rv = aWorkerConnection->GetSchemaVersion(&schemaVer);
NS_ENSURE_SUCCESS(rv, rv);
// downgrade (v0) -> upgrade (v1+) specific code
if (schemaVer >= 1) {
bool schema0IndexExists;
rv = aWorkerConnection->IndexExists("scope_key_index"_ns,
&schema0IndexExists);
NS_ENSURE_SUCCESS(rv, rv);
if (schema0IndexExists) {
// If this index exists, the database (already updated to schema >1)
// has been run again on schema 0 code. That recreated that index
// and might store some new rows while updating only the 'scope' column.
// For such added rows we must fill the new 'origin*' columns correctly
// otherwise there would be a data loss. The safest way to do it is to
// simply run the whole update to schema 1 again.
schemaVer = 0;
}
}
switch (schemaVer) {
case 0: {
bool webappsstore2Exists, webappsstoreExists, moz_webappsstoreExists;
rv = aWorkerConnection->TableExists("webappsstore2"_ns,
&webappsstore2Exists);
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->TableExists("webappsstore"_ns,
&webappsstoreExists);
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->TableExists("moz_webappsstore"_ns,
&moz_webappsstoreExists);
NS_ENSURE_SUCCESS(rv, rv);
if (!webappsstore2Exists && !webappsstoreExists &&
!moz_webappsstoreExists) {
// The database is empty, this is the first start. Just create the
// schema table and break to the next version to update to, i.e. bypass
// update from the old version.
rv = CreateSchema1Tables(aWorkerConnection);
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->SetSchemaVersion(CURRENT_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
break;
}
doVacuum = true;
// Ensure Gecko 1.9.1 storage table
rv = aWorkerConnection->ExecuteSimpleSQL(
nsLiteralCString("CREATE TABLE IF NOT EXISTS webappsstore2 ("
"scope TEXT, "
"key TEXT, "
"value TEXT, "
"secure INTEGER, "
"owner TEXT)"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(
nsLiteralCString("CREATE UNIQUE INDEX IF NOT EXISTS scope_key_index"
" ON webappsstore2(scope, key)"));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStorageFunction> function1(new nsReverseStringSQLFunction());
NS_ENSURE_TRUE(function1, NS_ERROR_OUT_OF_MEMORY);
rv = aWorkerConnection->CreateFunction("REVERSESTRING"_ns, 1, function1);
NS_ENSURE_SUCCESS(rv, rv);
// Check if there is storage of Gecko 1.9.0 and if so, upgrade that
// storage to actual webappsstore2 table and drop the obsolete table.
// First process this newer table upgrade to priority potential duplicates
// from older storage table.
if (webappsstoreExists) {
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"INSERT OR IGNORE INTO "
"webappsstore2(scope, key, value, secure, owner) "
"SELECT REVERSESTRING(domain) || '.:', key, value, secure, owner "
"FROM webappsstore"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL("DROP TABLE webappsstore"_ns);
NS_ENSURE_SUCCESS(rv, rv);
}
// Check if there is storage of Gecko 1.8 and if so, upgrade that storage
// to actual webappsstore2 table and drop the obsolete table. Potential
// duplicates will be ignored.
if (moz_webappsstoreExists) {
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"INSERT OR IGNORE INTO "
"webappsstore2(scope, key, value, secure, owner) "
"SELECT REVERSESTRING(domain) || '.:', key, value, secure, domain "
"FROM moz_webappsstore"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(
"DROP TABLE moz_webappsstore"_ns);
NS_ENSURE_SUCCESS(rv, rv);
}
aWorkerConnection->RemoveFunction("REVERSESTRING"_ns);
// Update the scoping to match the new implememntation: split to oa suffix
// and origin key First rename the old table, we want to remove some
// columns no longer needed, but even before that drop all indexes from it
// (CREATE IF NOT EXISTS for index on the new table would falsely find the
// index!)
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"DROP INDEX IF EXISTS webappsstore2.origin_key_index"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"DROP INDEX IF EXISTS webappsstore2.scope_key_index"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"ALTER TABLE webappsstore2 RENAME TO webappsstore2_old"));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStorageFunction> oaSuffixFunc(new GetOriginParticular(
GetOriginParticular::ORIGIN_ATTRIBUTES_SUFFIX));
rv = aWorkerConnection->CreateFunction("GET_ORIGIN_SUFFIX"_ns, 1,
oaSuffixFunc);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStorageFunction> originKeyFunc(
new GetOriginParticular(GetOriginParticular::ORIGIN_KEY));
rv = aWorkerConnection->CreateFunction("GET_ORIGIN_KEY"_ns, 1,
originKeyFunc);
NS_ENSURE_SUCCESS(rv, rv);
// Here we ensure this schema tables when we are updating.
rv = CreateSchema1Tables(aWorkerConnection);
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"INSERT OR IGNORE INTO "
"webappsstore2 (originAttributes, originKey, scope, key, value) "
"SELECT GET_ORIGIN_SUFFIX(scope), GET_ORIGIN_KEY(scope), scope, key, "
"value "
"FROM webappsstore2_old"));
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(
"DROP TABLE webappsstore2_old"_ns);
NS_ENSURE_SUCCESS(rv, rv);
aWorkerConnection->RemoveFunction("GET_ORIGIN_SUFFIX"_ns);
aWorkerConnection->RemoveFunction("GET_ORIGIN_KEY"_ns);
rv = aWorkerConnection->SetSchemaVersion(1);
NS_ENSURE_SUCCESS(rv, rv);
[[fallthrough]];
}
case 1: {
nsCOMPtr<mozIStorageFunction> oaStripAddonId(new StripOriginAddonId());
rv = aWorkerConnection->CreateFunction("STRIP_ADDON_ID"_ns, 1,
oaStripAddonId);
NS_ENSURE_SUCCESS(rv, rv);
rv = aWorkerConnection->ExecuteSimpleSQL(nsLiteralCString(
"UPDATE webappsstore2 "
"SET originAttributes = STRIP_ADDON_ID(originAttributes) "
"WHERE originAttributes LIKE '^%'"));
NS_ENSURE_SUCCESS(rv, rv);
aWorkerConnection->RemoveFunction("STRIP_ADDON_ID"_ns);
rv = aWorkerConnection->SetSchemaVersion(2);
NS_ENSURE_SUCCESS(rv, rv);
[[fallthrough]];
}
case CURRENT_SCHEMA_VERSION:
// Ensure the tables and indexes are up. This is mostly a no-op
// in common scenarios.
rv = CreateSchema1Tables(aWorkerConnection);
NS_ENSURE_SUCCESS(rv, rv);
// Nothing more to do here, this is the current schema version
break;
default:
MOZ_ASSERT(false);
break;
} // switch
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
if (doVacuum) {
// In some cases this can make the disk file of the database significantly
// smaller. VACUUM cannot be executed inside a transaction.
rv = aWorkerConnection->ExecuteSimpleSQL("VACUUM"_ns);
NS_ENSURE_SUCCESS(rv, rv);
}
return NS_OK;
}
} // namespace StorageDBUpdater
} // namespace dom
} // namespace mozilla