Source code

Revision control

Copy as Markdown

Other Tools

/* 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 "mozilla/ArrayUtils.h"
#include "mozilla/Attributes.h"
#include "mozilla/DebugOnly.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/SpinEventLoopUntil.h"
#include "mozilla/JSONWriter.h"
#include "Database.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIFile.h"
#include "nsNavBookmarks.h"
#include "nsNavHistory.h"
#include "nsPlacesTables.h"
#include "nsPlacesIndexes.h"
#include "nsPlacesTriggers.h"
#include "nsPlacesMacros.h"
#include "nsVariant.h"
#include "SQLFunctions.h"
#include "Helpers.h"
#include "nsFaviconService.h"
#include "nsAppDirectoryServiceDefs.h"
#include "nsDirectoryServiceUtils.h"
#include "prenv.h"
#include "prsystem.h"
#include "nsPrintfCString.h"
#include "mozilla/Preferences.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"
#include "mozIStorageService.h"
#include "prtime.h"
#include "nsXULAppAPI.h"
// Time between corrupt database backups.
#define RECENT_BACKUP_TIME_MICROSEC (int64_t)86400 * PR_USEC_PER_SEC // 24H
// Filename of the database.
#define DATABASE_FILENAME u"places.sqlite"_ns
// Filename of the icons database.
#define DATABASE_FAVICONS_FILENAME u"favicons.sqlite"_ns
// Set to the database file name when it was found corrupt by a previous
// maintenance run.
#define PREF_FORCE_DATABASE_REPLACEMENT \
"places.database.replaceDatabaseOnStartup"
// Whether on corruption we should try to fix the database by cloning it.
#define PREF_DATABASE_CLONEONCORRUPTION "places.database.cloneOnCorruption"
// Set to specify the size of the places database growth increments in kibibytes
#define PREF_GROWTH_INCREMENT_KIB "places.database.growthIncrementKiB"
// Set to disable the default robust storage and use volatile, in-memory
// storage without robust transaction flushing guarantees. This makes
// SQLite use much less I/O at the cost of losing data when things crash.
// The pref is only honored if an environment variable is set. The env
// variable is intentionally named something scary to help prevent someone
// from thinking it is a useful performance optimization they should enable.
#define PREF_DISABLE_DURABILITY "places.database.disableDurability"
#define PREF_PREVIEWS_ENABLED "places.previews.enabled"
#define ENV_ALLOW_CORRUPTION \
"ALLOW_PLACES_DATABASE_TO_LOSE_DATA_AND_BECOME_CORRUPT"
#define PREF_MIGRATE_V52_ORIGIN_FRECENCIES \
"places.database.migrateV52OriginFrecencies"
// Maximum size for the WAL file.
// For performance reasons this should be as large as possible, so that more
// transactions can fit into it, and the checkpoint cost is paid less often.
// At the same time, since we use synchronous = NORMAL, an fsync happens only
// at checkpoint time, so we don't want the WAL to grow too much and risk to
// lose all the contained transactions on a crash.
#define DATABASE_MAX_WAL_BYTES 2048000
// Since exceeding the journal limit will cause a truncate, we allow a slightly
// larger limit than DATABASE_MAX_WAL_BYTES to reduce the number of truncates.
// This is the number of bytes the journal can grow over the maximum wal size
// before being truncated.
#define DATABASE_JOURNAL_OVERHEAD_BYTES 2048000
#define BYTES_PER_KIBIBYTE 1024
// How much time Sqlite can wait before returning a SQLITE_BUSY error.
#define DATABASE_BUSY_TIMEOUT_MS 100
// This annotation is no longer used & is obsolete, but here for migration.
#define LAST_USED_ANNO "bookmarkPropertiesDialog/folderLastUsed"_ns
// This is key in the meta table that the LAST_USED_ANNO is migrated to.
#define LAST_USED_FOLDERS_META_KEY "places/bookmarks/edit/lastusedfolder"_ns
// We use a fixed title for the mobile root to avoid marking the database as
// corrupt if we can't look up the localized title in the string bundle. Sync
// sets the title to the localized version when it creates the left pane query.
#define MOBILE_ROOT_TITLE "mobile"
// Legacy item annotation used by the old Sync engine.
#define SYNC_PARENT_ANNO "sync/parent"
using namespace mozilla;
namespace mozilla {
namespace places {
namespace {
////////////////////////////////////////////////////////////////////////////////
//// Helpers
/**
* Get the filename for a corrupt database.
*/
nsString getCorruptFilename(const nsString& aDbFilename) {
return aDbFilename + u".corrupt"_ns;
}
/**
* Get the filename for a recover database.
*/
nsString getRecoverFilename(const nsString& aDbFilename) {
return aDbFilename + u".recover"_ns;
}
/**
* Checks whether exists a corrupt database file created not longer than
* RECENT_BACKUP_TIME_MICROSEC ago.
*/
bool isRecentCorruptFile(const nsCOMPtr<nsIFile>& aCorruptFile) {
MOZ_ASSERT(NS_IsMainThread());
bool fileExists = false;
if (NS_FAILED(aCorruptFile->Exists(&fileExists)) || !fileExists) {
return false;
}
PRTime lastMod = 0;
if (NS_FAILED(aCorruptFile->GetLastModifiedTime(&lastMod)) || lastMod <= 0 ||
(PR_Now() - lastMod) > RECENT_BACKUP_TIME_MICROSEC) {
return false;
}
return true;
}
/**
* Sets the connection journal mode to one of the JOURNAL_* types.
*
* @param aDBConn
* The database connection.
* @param aJournalMode
* One of the JOURNAL_* types.
* @returns the current journal mode.
* @note this may return a different journal mode than the required one, since
* setting it may fail.
*/
enum JournalMode SetJournalMode(nsCOMPtr<mozIStorageConnection>& aDBConn,
enum JournalMode aJournalMode) {
MOZ_ASSERT(NS_IsMainThread());
nsAutoCString journalMode;
switch (aJournalMode) {
default:
MOZ_FALLTHROUGH_ASSERT("Trying to set an unknown journal mode.");
// Fall through to the default DELETE journal.
case JOURNAL_DELETE:
journalMode.AssignLiteral("delete");
break;
case JOURNAL_TRUNCATE:
journalMode.AssignLiteral("truncate");
break;
case JOURNAL_MEMORY:
journalMode.AssignLiteral("memory");
break;
case JOURNAL_WAL:
journalMode.AssignLiteral("wal");
break;
}
nsCOMPtr<mozIStorageStatement> statement;
nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA journal_mode = ");
query.Append(journalMode);
aDBConn->CreateStatement(query, getter_AddRefs(statement));
NS_ENSURE_TRUE(statement, JOURNAL_DELETE);
bool hasResult = false;
if (NS_SUCCEEDED(statement->ExecuteStep(&hasResult)) && hasResult &&
NS_SUCCEEDED(statement->GetUTF8String(0, journalMode))) {
if (journalMode.EqualsLiteral("delete")) {
return JOURNAL_DELETE;
}
if (journalMode.EqualsLiteral("truncate")) {
return JOURNAL_TRUNCATE;
}
if (journalMode.EqualsLiteral("memory")) {
return JOURNAL_MEMORY;
}
if (journalMode.EqualsLiteral("wal")) {
return JOURNAL_WAL;
}
MOZ_ASSERT(false, "Got an unknown journal mode.");
}
return JOURNAL_DELETE;
}
nsresult CreateRoot(nsCOMPtr<mozIStorageConnection>& aDBConn,
const nsCString& aRootName, const nsCString& aGuid,
const nsCString& titleString, const int32_t position,
int64_t& newId) {
MOZ_ASSERT(NS_IsMainThread());
// A single creation timestamp for all roots so that the root folder's
// last modification time isn't earlier than its childrens' creation time.
static PRTime timestamp = 0;
if (!timestamp) timestamp = RoundedPRNow();
// Create a new bookmark folder for the root.
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = aDBConn->CreateStatement(
nsLiteralCString(
"INSERT INTO moz_bookmarks "
"(type, position, title, dateAdded, lastModified, guid, parent, "
"syncChangeCounter, syncStatus) "
"VALUES (:item_type, :item_position, :item_title,"
":date_added, :last_modified, :guid, "
"IFNULL((SELECT id FROM moz_bookmarks WHERE parent = 0), 0), "
"1, :sync_status)"),
getter_AddRefs(stmt));
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt32ByName("item_type"_ns,
nsINavBookmarksService::TYPE_FOLDER);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt32ByName("item_position"_ns, position);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindUTF8StringByName("item_title"_ns, titleString);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt64ByName("date_added"_ns, timestamp);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt64ByName("last_modified"_ns, timestamp);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindUTF8StringByName("guid"_ns, aGuid);
if (NS_FAILED(rv)) return rv;
rv = stmt->BindInt32ByName("sync_status"_ns,
nsINavBookmarksService::SYNC_STATUS_NEW);
if (NS_FAILED(rv)) return rv;
rv = stmt->Execute();
if (NS_FAILED(rv)) return rv;
newId = nsNavBookmarks::sLastInsertedItemId;
return NS_OK;
}
nsresult SetupDurability(nsCOMPtr<mozIStorageConnection>& aDBConn,
int32_t aDBPageSize) {
nsresult rv;
if (PR_GetEnv(ENV_ALLOW_CORRUPTION) &&
Preferences::GetBool(PREF_DISABLE_DURABILITY, false)) {
// Volatile storage was requested. Use the in-memory journal (no
// filesystem I/O) and don't sync the filesystem after writing.
SetJournalMode(aDBConn, JOURNAL_MEMORY);
rv = aDBConn->ExecuteSimpleSQL("PRAGMA synchronous = OFF"_ns);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Be sure to set journal mode after page_size. WAL would prevent the
// change otherwise.
if (JOURNAL_WAL == SetJournalMode(aDBConn, JOURNAL_WAL)) {
// Set the WAL journal size limit.
int32_t checkpointPages =
static_cast<int32_t>(DATABASE_MAX_WAL_BYTES / aDBPageSize);
nsAutoCString checkpointPragma("PRAGMA wal_autocheckpoint = ");
checkpointPragma.AppendInt(checkpointPages);
rv = aDBConn->ExecuteSimpleSQL(checkpointPragma);
NS_ENSURE_SUCCESS(rv, rv);
} else {
// Ignore errors, if we fail here the database could be considered corrupt
// and we won't be able to go on, even if it's just matter of a bogus file
// system. The default mode (DELETE) will be fine in such a case.
(void)SetJournalMode(aDBConn, JOURNAL_TRUNCATE);
// Set synchronous to FULL to ensure maximum data integrity, even in
// case of crashes or unclean shutdowns.
rv = aDBConn->ExecuteSimpleSQL("PRAGMA synchronous = FULL"_ns);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// The journal is usually free to grow for performance reasons, but it never
// shrinks back. Since the space taken may be problematic, limit its size.
nsAutoCString journalSizePragma("PRAGMA journal_size_limit = ");
journalSizePragma.AppendInt(DATABASE_MAX_WAL_BYTES +
DATABASE_JOURNAL_OVERHEAD_BYTES);
(void)aDBConn->ExecuteSimpleSQL(journalSizePragma);
// Grow places in |growthIncrementKiB| increments to limit fragmentation on
// disk. By default, it's 5 MB.
int32_t growthIncrementKiB =
Preferences::GetInt(PREF_GROWTH_INCREMENT_KIB, 5 * BYTES_PER_KIBIBYTE);
if (growthIncrementKiB > 0) {
(void)aDBConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE,
""_ns);
}
return NS_OK;
}
nsresult AttachDatabase(nsCOMPtr<mozIStorageConnection>& aDBConn,
const nsACString& aPath, const nsACString& aName) {
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = aDBConn->CreateStatement("ATTACH DATABASE :path AS "_ns + aName,
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName("path"_ns, aPath);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// The journal limit must be set apart for each database.
nsAutoCString journalSizePragma("PRAGMA favicons.journal_size_limit = ");
journalSizePragma.AppendInt(DATABASE_MAX_WAL_BYTES +
DATABASE_JOURNAL_OVERHEAD_BYTES);
Unused << aDBConn->ExecuteSimpleSQL(journalSizePragma);
return NS_OK;
}
} // namespace
////////////////////////////////////////////////////////////////////////////////
//// Database
PLACES_FACTORY_SINGLETON_IMPLEMENTATION(Database, gDatabase)
NS_IMPL_ISUPPORTS(Database, nsIObserver, nsISupportsWeakReference)
Database::Database()
: mMainThreadStatements(mMainConn),
mMainThreadAsyncStatements(mMainConn),
mAsyncThreadStatements(mMainConn),
mDBPageSize(0),
mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK),
mClosed(false),
mClientsShutdown(new ClientsShutdownBlocker()),
mConnectionShutdown(new ConnectionShutdownBlocker(this)),
mMaxUrlLength(0),
mCacheObservers(TOPIC_PLACES_INIT_COMPLETE),
mRootId(-1),
mMenuRootId(-1),
mTagsRootId(-1),
mUnfiledRootId(-1),
mToolbarRootId(-1),
mMobileRootId(-1) {
MOZ_ASSERT(!XRE_IsContentProcess(),
"Cannot instantiate Places in the content process");
// Attempting to create two instances of the service?
MOZ_ASSERT(!gDatabase);
gDatabase = this;
}
already_AddRefed<nsIAsyncShutdownClient>
Database::GetProfileChangeTeardownPhase() {
nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc =
services::GetAsyncShutdownService();
MOZ_ASSERT(asyncShutdownSvc);
if (NS_WARN_IF(!asyncShutdownSvc)) {
return nullptr;
}
// Consumers of Places should shutdown before us, at profile-change-teardown.
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
DebugOnly<nsresult> rv =
asyncShutdownSvc->GetProfileChangeTeardown(getter_AddRefs(shutdownPhase));
MOZ_ASSERT(NS_SUCCEEDED(rv));
return shutdownPhase.forget();
}
already_AddRefed<nsIAsyncShutdownClient>
Database::GetProfileBeforeChangePhase() {
nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc =
services::GetAsyncShutdownService();
MOZ_ASSERT(asyncShutdownSvc);
if (NS_WARN_IF(!asyncShutdownSvc)) {
return nullptr;
}
// Consumers of Places should shutdown before us, at profile-change-teardown.
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
DebugOnly<nsresult> rv =
asyncShutdownSvc->GetProfileBeforeChange(getter_AddRefs(shutdownPhase));
MOZ_ASSERT(NS_SUCCEEDED(rv));
return shutdownPhase.forget();
}
Database::~Database() = default;
already_AddRefed<mozIStorageAsyncStatement> Database::GetAsyncStatement(
const nsACString& aQuery) {
if (PlacesShutdownBlocker::sIsStarted || NS_FAILED(EnsureConnection())) {
return nullptr;
}
MOZ_ASSERT(NS_IsMainThread());
return mMainThreadAsyncStatements.GetCachedStatement(aQuery);
}
already_AddRefed<mozIStorageStatement> Database::GetStatement(
const nsACString& aQuery) {
if (PlacesShutdownBlocker::sIsStarted) {
return nullptr;
}
if (NS_IsMainThread()) {
if (NS_FAILED(EnsureConnection())) {
return nullptr;
}
return mMainThreadStatements.GetCachedStatement(aQuery);
}
// In the async case, the connection must have been started on the main-thread
// already.
MOZ_ASSERT(mMainConn);
return mAsyncThreadStatements.GetCachedStatement(aQuery);
}
already_AddRefed<nsIAsyncShutdownClient> Database::GetClientsShutdown() {
if (mClientsShutdown) return mClientsShutdown->GetClient();
return nullptr;
}
already_AddRefed<nsIAsyncShutdownClient> Database::GetConnectionShutdown() {
if (mConnectionShutdown) return mConnectionShutdown->GetClient();
return nullptr;
}
// static
already_AddRefed<Database> Database::GetDatabase() {
if (PlacesShutdownBlocker::sIsStarted) {
return nullptr;
}
return GetSingleton();
}
nsresult Database::Init() {
MOZ_ASSERT(NS_IsMainThread());
// DO NOT FAIL HERE, otherwise we would never break the cycle between this
// object and the shutdown blockers, causing unexpected leaks.
{
// First of all Places clients should block profile-change-teardown.
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase =
GetProfileChangeTeardownPhase();
MOZ_ASSERT(shutdownPhase);
if (shutdownPhase) {
DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
static_cast<nsIAsyncShutdownBlocker*>(mClientsShutdown.get()),
NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
{
// Then connection closing should block profile-before-change.
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase =
GetProfileBeforeChangePhase();
MOZ_ASSERT(shutdownPhase);
if (shutdownPhase) {
DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
static_cast<nsIAsyncShutdownBlocker*>(mConnectionShutdown.get()),
NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, u""_ns);
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
// Finally observe profile shutdown notifications.
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
if (os) {
(void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true);
}
return NS_OK;
}
nsresult Database::EnsureConnection() {
// Run this only once.
if (mMainConn ||
mDatabaseStatus == nsINavHistoryService::DATABASE_STATUS_LOCKED) {
return NS_OK;
}
// Don't try to create a database too late.
if (PlacesShutdownBlocker::sIsStarted) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(NS_IsMainThread(),
"Database initialization must happen on the main-thread");
{
bool initSucceeded = false;
auto notify = MakeScopeExit([&]() {
// If the database connection cannot be opened, it may just be locked
// by third parties. Set a locked state.
if (!initSucceeded) {
mMainConn = nullptr;
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_LOCKED;
}
// Notify at the next tick, to avoid re-entrancy problems.
NS_DispatchToMainThread(
NewRunnableMethod("places::Database::EnsureConnection()", this,
&Database::NotifyConnectionInitalized));
});
nsCOMPtr<mozIStorageService> storage =
do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
NS_ENSURE_STATE(storage);
nsCOMPtr<nsIFile> profileDir;
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profileDir));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> databaseFile;
rv = profileDir->Clone(getter_AddRefs(databaseFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = databaseFile->Append(DATABASE_FILENAME);
NS_ENSURE_SUCCESS(rv, rv);
bool databaseExisted = false;
rv = databaseFile->Exists(&databaseExisted);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoString corruptDbName;
if (NS_SUCCEEDED(Preferences::GetString(PREF_FORCE_DATABASE_REPLACEMENT,
corruptDbName)) &&
!corruptDbName.IsEmpty()) {
// If this pref is set, maintenance required a database replacement, due
// to integrity corruption. Be sure to clear the pref to avoid handling it
// more than once.
(void)Preferences::ClearUser(PREF_FORCE_DATABASE_REPLACEMENT);
// The database is corrupt, backup and replace it with a new one.
nsCOMPtr<nsIFile> fileToBeReplaced;
bool fileExists = false;
if (NS_SUCCEEDED(profileDir->Clone(getter_AddRefs(fileToBeReplaced))) &&
NS_SUCCEEDED(fileToBeReplaced->Exists(&fileExists)) && fileExists) {
rv = BackupAndReplaceDatabaseFile(storage, corruptDbName, true, false);
NS_ENSURE_SUCCESS(rv, rv);
}
}
// Open the database file. If it does not exist a new one will be created.
// Use an unshared connection, it will consume more memory but avoid shared
// cache contentions across threads.
rv = storage->OpenUnsharedDatabase(databaseFile,
mozIStorageService::CONNECTION_DEFAULT,
getter_AddRefs(mMainConn));
if (NS_SUCCEEDED(rv) && !databaseExisted) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE;
} else if (rv == NS_ERROR_FILE_CORRUPTED) {
// The database is corrupt, backup and replace it with a new one.
rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FILENAME, true, true);
// Fallback to catch-all handler.
}
NS_ENSURE_SUCCESS(rv, rv);
// Initialize the database schema. In case of failure the existing schema
// is is corrupt or incoherent, thus the database should be replaced.
bool databaseMigrated = false;
rv = SetupDatabaseConnection(storage);
bool shouldTryToCloneDb = true;
if (NS_SUCCEEDED(rv)) {
// Failing to initialize the schema may indicate a corruption.
rv = InitSchema(&databaseMigrated);
if (NS_FAILED(rv)) {
// Cloning the db on a schema migration may not be a good idea, since we
// may end up cloning the schema problems.
shouldTryToCloneDb = false;
if (rv == NS_ERROR_STORAGE_BUSY || rv == NS_ERROR_FILE_IS_LOCKED ||
rv == NS_ERROR_FILE_NO_DEVICE_SPACE ||
rv == NS_ERROR_OUT_OF_MEMORY) {
// The database is not corrupt, though some migration step failed.
// This may be caused by concurrent use of sync and async Storage APIs
// or by a system issue.
// The best we can do is trying again. If it should still fail, Places
// won't work properly and will be handled as LOCKED.
rv = InitSchema(&databaseMigrated);
if (NS_FAILED(rv)) {
rv = NS_ERROR_FILE_IS_LOCKED;
}
} else {
rv = NS_ERROR_FILE_CORRUPTED;
}
}
}
if (NS_WARN_IF(NS_FAILED(rv))) {
if (rv != NS_ERROR_FILE_IS_LOCKED) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
}
// Some errors may not indicate a database corruption, for those cases we
// just bail out without throwing away a possibly valid places.sqlite.
if (rv == NS_ERROR_FILE_CORRUPTED) {
// Since we don't know which database is corrupt, we must replace both.
rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FAVICONS_FILENAME,
false, false);
NS_ENSURE_SUCCESS(rv, rv);
rv = BackupAndReplaceDatabaseFile(storage, DATABASE_FILENAME,
shouldTryToCloneDb, true);
NS_ENSURE_SUCCESS(rv, rv);
// Try to initialize the new database again.
rv = SetupDatabaseConnection(storage);
NS_ENSURE_SUCCESS(rv, rv);
rv = InitSchema(&databaseMigrated);
}
// Bail out if we couldn't fix the database.
NS_ENSURE_SUCCESS(rv, rv);
}
if (databaseMigrated) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_UPGRADED;
}
// Initialize here all the items that are not part of the on-disk database,
// like views, temp triggers or temp tables. The database should not be
// considered corrupt if any of the following fails.
rv = InitTempEntities();
NS_ENSURE_SUCCESS(rv, rv);
rv = CheckRoots();
NS_ENSURE_SUCCESS(rv, rv);
initSucceeded = true;
}
return NS_OK;
}
nsresult Database::NotifyConnectionInitalized() {
MOZ_ASSERT(NS_IsMainThread());
// Notify about Places initialization.
nsCOMArray<nsIObserver> entries;
mCacheObservers.GetEntries(entries);
for (int32_t idx = 0; idx < entries.Count(); ++idx) {
MOZ_ALWAYS_SUCCEEDS(
entries[idx]->Observe(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
}
nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
if (obs) {
MOZ_ALWAYS_SUCCEEDS(
obs->NotifyObservers(nullptr, TOPIC_PLACES_INIT_COMPLETE, nullptr));
}
return NS_OK;
}
nsresult Database::EnsureFaviconsDatabaseAttached(
const nsCOMPtr<mozIStorageService>& aStorage) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsIFile> databaseFile;
NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(databaseFile));
NS_ENSURE_STATE(databaseFile);
nsresult rv = databaseFile->Append(DATABASE_FAVICONS_FILENAME);
NS_ENSURE_SUCCESS(rv, rv);
nsString iconsPath;
rv = databaseFile->GetPath(iconsPath);
NS_ENSURE_SUCCESS(rv, rv);
bool fileExists = false;
if (NS_SUCCEEDED(databaseFile->Exists(&fileExists)) && fileExists) {
return AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
"favicons"_ns);
}
// Open the database file, this will also create it.
nsCOMPtr<mozIStorageConnection> conn;
rv = aStorage->OpenUnsharedDatabase(databaseFile,
mozIStorageService::CONNECTION_DEFAULT,
getter_AddRefs(conn));
NS_ENSURE_SUCCESS(rv, rv);
{
// Ensure we'll close the connection when done.
auto cleanup = MakeScopeExit([&]() {
// We cannot use AsyncClose() here, because by the time we try to ATTACH
// this database, its transaction could be still be running and that would
// cause the ATTACH query to fail.
MOZ_ALWAYS_TRUE(NS_SUCCEEDED(conn->Close()));
});
// Enable incremental vacuum for this database. Since it will contain even
// large blobs and can be cleared with history, it's worth to have it.
// Note that it will be necessary to manually use PRAGMA incremental_vacuum.
rv = conn->ExecuteSimpleSQL("PRAGMA auto_vacuum = INCREMENTAL"_ns);
NS_ENSURE_SUCCESS(rv, rv);
#if !defined(HAVE_64BIT_BUILD)
// Ensure that temp tables are held in memory, not on disk, on 32 bit
// platforms.
rv = conn->ExecuteSimpleSQL("PRAGMA temp_store = MEMORY"_ns);
NS_ENSURE_SUCCESS(rv, rv);
#endif
int32_t defaultPageSize;
rv = conn->GetDefaultPageSize(&defaultPageSize);
NS_ENSURE_SUCCESS(rv, rv);
rv = SetupDurability(conn, defaultPageSize);
NS_ENSURE_SUCCESS(rv, rv);
// We are going to update the database, so everything from now on should be
// in a transaction for performances.
mozStorageTransaction transaction(conn, false);
// XXX Handle the error, bug 1696133.
Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ICONS_ICONURLHASH);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(CREATE_MOZ_PAGES_W_ICONS);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PAGES_W_ICONS_ICONURLHASH);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(CREATE_MOZ_ICONS_TO_PAGES);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
// The scope exit will take care of closing the connection.
}
rv = AttachDatabase(mMainConn, NS_ConvertUTF16toUTF8(iconsPath),
"favicons"_ns);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::BackupAndReplaceDatabaseFile(
nsCOMPtr<mozIStorageService>& aStorage, const nsString& aDbFilename,
bool aTryToClone, bool aReopenConnection) {
MOZ_ASSERT(NS_IsMainThread());
if (aDbFilename.Equals(DATABASE_FILENAME)) {
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT;
} else {
// Due to OS file lockings, attached databases can't be cloned properly,
// otherwise trying to reattach them later would fail.
aTryToClone = false;
}
nsCOMPtr<nsIFile> profDir;
nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(profDir));
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> databaseFile;
rv = profDir->Clone(getter_AddRefs(databaseFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = databaseFile->Append(aDbFilename);
NS_ENSURE_SUCCESS(rv, rv);
// If we already failed in the last 24 hours avoid to create another corrupt
// file, since doing so, in some situation, could cause us to create a new
// corrupt file at every try to access any Places service. That is bad
// because it would quickly fill the user's disk space without any notice.
nsCOMPtr<nsIFile> corruptFile;
rv = profDir->Clone(getter_AddRefs(corruptFile));
NS_ENSURE_SUCCESS(rv, rv);
nsString corruptFilename = getCorruptFilename(aDbFilename);
rv = corruptFile->Append(corruptFilename);
NS_ENSURE_SUCCESS(rv, rv);
if (!isRecentCorruptFile(corruptFile)) {
// Ensure we never create more than one corrupt file.
nsCOMPtr<nsIFile> corruptFile;
rv = profDir->Clone(getter_AddRefs(corruptFile));
NS_ENSURE_SUCCESS(rv, rv);
nsString corruptFilename = getCorruptFilename(aDbFilename);
rv = corruptFile->Append(corruptFilename);
NS_ENSURE_SUCCESS(rv, rv);
rv = corruptFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
nsCOMPtr<nsIFile> backup;
Unused << aStorage->BackupDatabaseFile(databaseFile, corruptFilename,
profDir, getter_AddRefs(backup));
}
// If anything fails from this point on, we have a stale connection or
// database file, and there's not much more we can do.
// The only thing we can try to do is to replace the database on the next
// startup, and report the problem through telemetry.
{
enum eCorruptDBReplaceStage : int8_t {
stage_closing = 0,
stage_removing,
stage_reopening,
stage_replaced,
stage_cloning,
stage_cloned
};
eCorruptDBReplaceStage stage = stage_closing;
auto guard = MakeScopeExit([&]() {
if (stage != stage_replaced) {
// Reaching this point means the database is corrupt and we failed to
// replace it. For this session part of the application related to
// bookmarks and history will misbehave. The frontend may show a
// "locked" notification to the user though.
// Set up a pref to try replacing the database at the next startup.
Preferences::SetString(PREF_FORCE_DATABASE_REPLACEMENT, aDbFilename);
}
// Report the corruption through telemetry.
Telemetry::Accumulate(
Telemetry::PLACES_DATABASE_CORRUPTION_HANDLING_STAGE,
static_cast<int8_t>(stage));
});
// Close database connection if open.
if (mMainConn) {
rv = mMainConn->SpinningSynchronousClose();
NS_ENSURE_SUCCESS(rv, rv);
mMainConn = nullptr;
}
// Remove the broken database.
stage = stage_removing;
rv = databaseFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
// Create a new database file and try to clone tables from the corrupt one.
bool cloned = false;
if (aTryToClone &&
Preferences::GetBool(PREF_DATABASE_CLONEONCORRUPTION, true)) {
stage = stage_cloning;
rv = TryToCloneTablesFromCorruptDatabase(aStorage, databaseFile);
if (NS_SUCCEEDED(rv)) {
// If we cloned successfully, we should not consider the database
// corrupt anymore, otherwise we could reimport default bookmarks.
mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_OK;
cloned = true;
}
}
if (aReopenConnection) {
// Use an unshared connection, it will consume more memory but avoid
// shared cache contentions across threads.
stage = stage_reopening;
rv = aStorage->OpenUnsharedDatabase(
databaseFile, mozIStorageService::CONNECTION_DEFAULT,
getter_AddRefs(mMainConn));
NS_ENSURE_SUCCESS(rv, rv);
}
stage = cloned ? stage_cloned : stage_replaced;
}
return NS_OK;
}
nsresult Database::TryToCloneTablesFromCorruptDatabase(
const nsCOMPtr<mozIStorageService>& aStorage,
const nsCOMPtr<nsIFile>& aDatabaseFile) {
MOZ_ASSERT(NS_IsMainThread());
nsAutoString filename;
nsresult rv = aDatabaseFile->GetLeafName(filename);
nsCOMPtr<nsIFile> corruptFile;
rv = aDatabaseFile->Clone(getter_AddRefs(corruptFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = corruptFile->SetLeafName(getCorruptFilename(filename));
NS_ENSURE_SUCCESS(rv, rv);
nsAutoString path;
rv = corruptFile->GetPath(path);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIFile> recoverFile;
rv = aDatabaseFile->Clone(getter_AddRefs(recoverFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = recoverFile->SetLeafName(getRecoverFilename(filename));
NS_ENSURE_SUCCESS(rv, rv);
// Ensure there's no previous recover file.
rv = recoverFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
nsCOMPtr<mozIStorageConnection> conn;
auto guard = MakeScopeExit([&]() {
if (conn) {
Unused << conn->Close();
}
Unused << recoverFile->Remove(false);
});
rv = aStorage->OpenUnsharedDatabase(recoverFile,
mozIStorageService::CONNECTION_DEFAULT,
getter_AddRefs(conn));
NS_ENSURE_SUCCESS(rv, rv);
rv = AttachDatabase(conn, NS_ConvertUTF16toUTF8(path), "corrupt"_ns);
NS_ENSURE_SUCCESS(rv, rv);
mozStorageTransaction transaction(conn, false);
// XXX Handle the error, bug 1696133.
Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
// Copy the schema version.
nsCOMPtr<mozIStorageStatement> stmt;
(void)conn->CreateStatement("PRAGMA corrupt.user_version"_ns,
getter_AddRefs(stmt));
NS_ENSURE_TRUE(stmt, NS_ERROR_OUT_OF_MEMORY);
bool hasResult;
rv = stmt->ExecuteStep(&hasResult);
NS_ENSURE_SUCCESS(rv, rv);
int32_t schemaVersion = stmt->AsInt32(0);
rv = conn->SetSchemaVersion(schemaVersion);
NS_ENSURE_SUCCESS(rv, rv);
// Recreate the tables.
rv = conn->CreateStatement(
nsLiteralCString(
"SELECT name, sql FROM corrupt.sqlite_master "
"WHERE type = 'table' AND name BETWEEN 'moz_' AND 'moza'"),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
nsAutoCString name;
rv = stmt->GetUTF8String(0, name);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString query;
rv = stmt->GetUTF8String(1, query);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(query);
NS_ENSURE_SUCCESS(rv, rv);
// Copy the table contents.
rv = conn->ExecuteSimpleSQL("INSERT INTO main."_ns + name +
" SELECT * FROM corrupt."_ns + name);
if (NS_FAILED(rv)) {
rv = conn->ExecuteSimpleSQL("INSERT INTO main."_ns + name +
" SELECT * FROM corrupt."_ns + name +
" ORDER BY rowid DESC"_ns);
}
NS_ENSURE_SUCCESS(rv, rv);
}
// Recreate the indices. Doing this after data addition is faster.
rv = conn->CreateStatement(
nsLiteralCString(
"SELECT sql FROM corrupt.sqlite_master "
"WHERE type <> 'table' AND name BETWEEN 'moz_' AND 'moza'"),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
hasResult = false;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
nsAutoCString query;
rv = stmt->GetUTF8String(0, query);
NS_ENSURE_SUCCESS(rv, rv);
rv = conn->ExecuteSimpleSQL(query);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = stmt->Finalize();
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
Unused << conn->Close();
conn = nullptr;
rv = recoverFile->RenameTo(nullptr, filename);
NS_ENSURE_SUCCESS(rv, rv);
Unused << corruptFile->Remove(false);
guard.release();
return NS_OK;
}
nsresult Database::SetupDatabaseConnection(
nsCOMPtr<mozIStorageService>& aStorage) {
MOZ_ASSERT(NS_IsMainThread());
// Using immediate transactions allows the main connection to retry writes
// that fail with `SQLITE_BUSY` because a cloned connection has locked the
// database for writing.
nsresult rv = mMainConn->SetDefaultTransactionType(
mozIStorageConnection::TRANSACTION_IMMEDIATE);
NS_ENSURE_SUCCESS(rv, rv);
// WARNING: any statement executed before setting the journal mode must be
// finalized, since SQLite doesn't allow changing the journal mode if there
// is any outstanding statement.
{
// Get the page size. This may be different than the default if the
// database file already existed with a different page size.
nsCOMPtr<mozIStorageStatement> statement;
rv = mMainConn->CreateStatement(
nsLiteralCString(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA page_size"),
getter_AddRefs(statement));
NS_ENSURE_SUCCESS(rv, rv);
bool hasResult = false;
rv = statement->ExecuteStep(&hasResult);
NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_FILE_CORRUPTED);
rv = statement->GetInt32(0, &mDBPageSize);
NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && mDBPageSize > 0,
NS_ERROR_FILE_CORRUPTED);
}
#if !defined(HAVE_64BIT_BUILD)
// Ensure that temp tables are held in memory, not on disk, on 32 bit
// platforms.
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA temp_store = MEMORY"));
NS_ENSURE_SUCCESS(rv, rv);
#endif
rv = SetupDurability(mMainConn, mDBPageSize);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = ");
busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS);
(void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma);
// Enable FOREIGN KEY support. This is a strict requirement.
rv = mMainConn->ExecuteSimpleSQL(nsLiteralCString(
MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA foreign_keys = ON"));
NS_ENSURE_SUCCESS(rv, NS_ERROR_FILE_CORRUPTED);
#ifdef DEBUG
{
// There are a few cases where setting foreign_keys doesn't work:
// * in the middle of a multi-statement transaction
// * if the SQLite library in use doesn't support them
// Since we need foreign_keys, let's at least assert in debug mode.
nsCOMPtr<mozIStorageStatement> stmt;
mMainConn->CreateStatement("PRAGMA foreign_keys"_ns, getter_AddRefs(stmt));
bool hasResult = false;
if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
int32_t fkState = stmt->AsInt32(0);
MOZ_ASSERT(fkState, "Foreign keys should be enabled");
}
}
#endif
// Attach the favicons database to the main connection.
rv = EnsureFaviconsDatabaseAttached(aStorage);
if (NS_FAILED(rv)) {
// The favicons database may be corrupt. Try to replace and reattach it.
nsCOMPtr<nsIFile> iconsFile;
rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
getter_AddRefs(iconsFile));
NS_ENSURE_SUCCESS(rv, rv);
rv = iconsFile->Append(DATABASE_FAVICONS_FILENAME);
NS_ENSURE_SUCCESS(rv, rv);
rv = iconsFile->Remove(false);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
return rv;
}
rv = EnsureFaviconsDatabaseAttached(aStorage);
NS_ENSURE_SUCCESS(rv, rv);
}
// Create favicons temp entities.
rv = mMainConn->ExecuteSimpleSQL(CREATE_ICONS_AFTERINSERT_TRIGGER);
NS_ENSURE_SUCCESS(rv, rv);
// We use our functions during migration, so initialize them now.
rv = InitFunctions();
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::InitSchema(bool* aDatabaseMigrated) {
MOZ_ASSERT(NS_IsMainThread());
*aDatabaseMigrated = false;
// Get the database schema version.
int32_t currentSchemaVersion;
nsresult rv = mMainConn->GetSchemaVersion(&currentSchemaVersion);
NS_ENSURE_SUCCESS(rv, rv);
bool databaseInitialized = currentSchemaVersion > 0;
if (databaseInitialized && currentSchemaVersion == DATABASE_SCHEMA_VERSION) {
// The database is up to date and ready to go.
return NS_OK;
}
auto guard = MakeScopeExit([&]() {
// These run at the end of the migration, out of the transaction,
// regardless of its success.
MigrateV52OriginFrecencies();
});
// We are going to update the database, so everything from now on should be in
// a transaction for performances.
mozStorageTransaction transaction(mMainConn, false);
// XXX Handle the error, bug 1696133.
Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
if (databaseInitialized) {
// Migration How-to:
//
// 1. increment PLACES_SCHEMA_VERSION.
// 2. implement a method that performs upgrade to your version from the
// previous one.
//
// NOTE: The downgrade process is pretty much complicated by the fact old
// versions cannot know what a new version is going to implement.
// The only thing we will do for downgrades is setting back the schema
// version, so that next upgrades will run again the migration step.
if (currentSchemaVersion < DATABASE_SCHEMA_VERSION) {
*aDatabaseMigrated = true;
if (currentSchemaVersion < 43) {
// These are versions older than Firefox 60 ESR that are not supported
// anymore. In this case it's safer to just replace the database.
return NS_ERROR_FILE_CORRUPTED;
}
// Firefox 60 uses schema version 43. - This is an ESR.
if (currentSchemaVersion < 44) {
rv = MigrateV44Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 45) {
rv = MigrateV45Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 46) {
rv = MigrateV46Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 47) {
rv = MigrateV47Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 61 uses schema version 47.
if (currentSchemaVersion < 48) {
rv = MigrateV48Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 49) {
rv = MigrateV49Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 50) {
rv = MigrateV50Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 51) {
rv = MigrateV51Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 52) {
rv = MigrateV52Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 62 uses schema version 52.
// Firefox 68 uses schema version 52. - This is an ESR.
if (currentSchemaVersion < 53) {
rv = MigrateV53Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 69 uses schema version 53
// Firefox 78 uses schema version 53 - This is an ESR.
if (currentSchemaVersion < 54) {
rv = MigrateV54Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 81 uses schema version 54
if (currentSchemaVersion < 55) {
rv = MigrateV55Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 56) {
rv = MigrateV56Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 57) {
rv = MigrateV57Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 91 uses schema version 57
if (currentSchemaVersion < 58) {
rv = MigrateV58Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 92 uses schema version 58
if (currentSchemaVersion < 59) {
rv = MigrateV59Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 94 uses schema version 59
if (currentSchemaVersion < 60) {
rv = MigrateV60Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 96 uses schema version 60
if (currentSchemaVersion < 61) {
rv = MigrateV61Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 62) {
rv = MigrateV62Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 97 uses schema version 62
if (currentSchemaVersion < 63) {
rv = MigrateV63Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 98 uses schema version 63
if (currentSchemaVersion < 64) {
rv = MigrateV64Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 99 uses schema version 64
if (currentSchemaVersion < 65) {
rv = MigrateV65Up();
NS_ENSURE_SUCCESS(rv, rv);
}
if (currentSchemaVersion < 66) {
rv = MigrateV66Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 100 uses schema version 66
// Schema Upgrades must add migration code here.
// >>> IMPORTANT! <<<
// NEVER MIX UP SYNC AND ASYNC EXECUTION IN MIGRATORS, YOU MAY LOCK THE
// CONNECTION AND CAUSE FURTHER STEPS TO FAIL.
// In case, set a bool and do the async work in the ScopeExit guard just
// before the migration steps.
}
} else {
// This is a new database, so we have to create all the tables and indices.
// moz_origins.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ORIGINS);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_URL_HASH);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_REVHOST);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_VISITCOUNT);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_FRECENCY);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_LASTVISITDATE);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_GUID);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_ORIGIN_ID);
NS_ENSURE_SUCCESS(rv, rv);
// moz_historyvisits.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HISTORYVISITS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_PLACEDATE);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_FROMVISIT);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_HISTORYVISITS_VISITDATE);
NS_ENSURE_SUCCESS(rv, rv);
// moz_inputhistory.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_INPUTHISTORY);
NS_ENSURE_SUCCESS(rv, rv);
// moz_bookmarks.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS_DELETED);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACETYPE);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PARENTPOSITION);
NS_ENSURE_SUCCESS(rv, rv);
rv =
mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_PLACELASTMODIFIED);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_DATEADDED);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_BOOKMARKS_GUID);
NS_ENSURE_SUCCESS(rv, rv);
// moz_keywords.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_KEYWORDS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA);
NS_ENSURE_SUCCESS(rv, rv);
// moz_anno_attributes.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ANNO_ATTRIBUTES);
NS_ENSURE_SUCCESS(rv, rv);
// moz_annos.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ANNOS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ANNOS_PLACEATTRIBUTE);
NS_ENSURE_SUCCESS(rv, rv);
// moz_items_annos.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_ITEMS_ANNOS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_ITEMSANNOS_PLACEATTRIBUTE);
NS_ENSURE_SUCCESS(rv, rv);
// moz_meta.
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_META);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_METADATA);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(
CREATE_IDX_MOZ_PLACES_METADATA_PLACECREATED);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_METADATA_REFERRER);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata_search_queries
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_METADATA_SEARCH_QUERIES);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata_snapshots
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_METADATA_SNAPSHOTS);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(
CREATE_IDX_MOZ_PLACES_METADATA_SNAPSHOTS_PINNNED);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata_snapshots_extra
rv =
mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PLACES_METADATA_SNAPSHOTS_EXTRA);
NS_ENSURE_SUCCESS(rv, rv);
rv = mMainConn->ExecuteSimpleSQL(
CREATE_IDX_MOZ_PLACES_METADATA_SNAPSHOTS_EXTRA_TYPE);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata_snapshots_groups
rv = mMainConn->ExecuteSimpleSQL(
CREATE_MOZ_PLACES_METADATA_SNAPSHOTS_GROUPS);
NS_ENSURE_SUCCESS(rv, rv);
// moz_places_metadata_groups_to_snapshots
rv = mMainConn->ExecuteSimpleSQL(
CREATE_MOZ_PLACES_METADATA_GROUPS_TO_SNAPSHOTS);
NS_ENSURE_SUCCESS(rv, rv);
// moz_session_metadata
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_SESSION_METADATA);
NS_ENSURE_SUCCESS(rv, rv);
// moz_session_to_places
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_SESSION_TO_PLACES);
NS_ENSURE_SUCCESS(rv, rv);
// moz_previews_tombstones
rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_PREVIEWS_TOMBSTONES);
NS_ENSURE_SUCCESS(rv, rv);
// The bookmarks roots get initialized in CheckRoots().
}
// Set the schema version to the current one.
rv = mMainConn->SetSchemaVersion(DATABASE_SCHEMA_VERSION);
NS_ENSURE_SUCCESS(rv, rv);
rv = transaction.Commit();
NS_ENSURE_SUCCESS(rv, rv);
// ANY FAILURE IN THIS METHOD WILL CAUSE US TO MARK THE DATABASE AS CORRUPT
// AND TRY TO REPLACE IT.
// DO NOT PUT HERE ANYTHING THAT IS NOT RELATED TO INITIALIZATION OR MODIFYING
// THE DISK DATABASE.
return NS_OK;
}
nsresult Database::CheckRoots() {
MOZ_ASSERT(NS_IsMainThread());
// If the database has just been created, skip straight to the part where
// we create the roots.
if (mDatabaseStatus == nsINavHistoryService::DATABASE_STATUS_CREATE) {
return EnsureBookmarkRoots(0, /* shouldReparentRoots */ false);
}
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mMainConn->CreateStatement(
nsLiteralCString("SELECT guid, id, position, parent FROM moz_bookmarks "
"WHERE guid IN ( "
"'" ROOT_GUID "', '" MENU_ROOT_GUID
"', '" TOOLBAR_ROOT_GUID "', "
"'" TAGS_ROOT_GUID "', '" UNFILED_ROOT_GUID
"', '" MOBILE_ROOT_GUID "' )"),
getter_AddRefs(stmt));
NS_ENSURE_SUCCESS(rv, rv);
bool hasResult;
nsAutoCString guid;
int32_t maxPosition = 0;
bool shouldReparentRoots = false;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
rv = stmt->GetUTF8String(0, guid);
NS_ENSURE_SUCCESS(rv, rv);
int64_t parentId = stmt->AsInt64(3);
if (guid.EqualsLiteral(ROOT_GUID)) {
mRootId = stmt->AsInt64(1);
shouldReparentRoots |= parentId != 0;
} else {
maxPosition = std::max(stmt->AsInt32(2), maxPosition);
if (guid.EqualsLiteral(MENU_ROOT_GUID)) {
mMenuRootId = stmt->AsInt64(1);
} else if (guid.EqualsLiteral(TOOLBAR_ROOT_GUID)) {
mToolbarRootId = stmt->AsInt64(1);
} else if (guid.EqualsLiteral(TAGS_ROOT_GUID)) {
mTagsRootId = stmt->AsInt64(1);
} else if (guid.EqualsLiteral(UNFILED_ROOT_GUID)) {
mUnfiledRootId = stmt->AsInt64(1);
} else if (guid.EqualsLiteral(MOBILE_ROOT_GUID)) {
mMobileRootId = stmt->AsInt64(1);
}
shouldReparentRoots |= parentId != mRootId;
}
}
rv = EnsureBookmarkRoots(maxPosition + 1, shouldReparentRoots);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult Database::EnsureBookmarkRoots(const int32_t startPosition,
bool shouldReparentRoots) {
MOZ_ASSERT(NS_IsMainThread());
nsresult rv;
if (mRootId < 1) {
// The first root's title is an empty string.
rv = CreateRoot(mMainConn, "places"_ns, "root________"_ns, ""_ns, 0,
mRootId);
if (NS_FAILED(rv)) return rv;
}
int32_t position = startPosition;
// For the other roots, the UI doesn't rely on the value in the database, so
// just set it to something simple to make it easier for humans to read.
if (mMenuRootId < 1) {
rv = CreateRoot(mMainConn, "menu"_ns, "menu________"_ns, "menu"_ns,
position, mMenuRootId);
if (NS_FAILED(rv)) return rv;
position++;
}
if (mToolbarRootId < 1) {
rv = CreateRoot(mMainConn, "toolbar"_ns, "toolbar_____"_ns, "toolbar"_ns,
position, mToolbarRootId);
if (NS_FAILED(rv)) return rv;
position++;
}
if (mTagsRootId < 1) {
rv = CreateRoot(mMainConn, "tags"_ns, "tags________"_ns, "tags"_ns,
position, mTagsRootId);
if (NS_FAILED(rv)) return rv;
position++;
}
if (mUnfiledRootId < 1) {
rv = CreateRoot(mMainConn, "unfiled"_ns, "unfiled_____"_ns, "unfiled"_ns,
position, mUnfiledRootId);
if (NS_FAILED(rv)) return rv;
position++;
}
if (mMobileRootId < 1) {
int64_t mobileRootId = CreateMobileRoot();
if (mobileRootId <= 0) return NS_ERROR_FAILURE;
{
nsCOMPtr<mozIStorageStatement> mobileRootSyncStatusStmt;
rv = mMainConn->CreateStatement(
nsLiteralCString("UPDATE moz_bookmarks SET syncStatus = "
":sync_status WHERE id = :id"),
getter_AddRefs(mobileRootSyncStatusStmt));
if (NS_FAILED(rv)) return rv;
rv = mobileRootSyncStatusStmt->BindInt32ByName(
"sync_status"_ns, nsINavBookmarksService::SYNC_STATUS_NEW);
if (NS_FAILED(rv)) return rv;
rv = mobileRootSyncStatusStmt->BindInt64ByName("id"_ns, mobileRootId);
if (NS_FAILED(rv)) return rv;
rv = mobileRootSyncStatusStmt->Execute();
if (NS_FAILED(rv)) return rv;
mMobileRootId = mobileRootId;
}