/* 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 "Database.h" #include "nsIAnnotationService.h" #include "nsINavBookmarksService.h" #include "nsIInterfaceRequestorUtils.h" #include "nsIFile.h" #include "nsIWritablePropertyBag2.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 "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 "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 NS_LITERAL_STRING("places.sqlite") // Filename used to backup corrupt databases. #define DATABASE_CORRUPT_FILENAME NS_LITERAL_STRING("places.sqlite.corrupt") // Set when the database file was found corrupt by a previous maintenance. #define PREF_FORCE_DATABASE_REPLACEMENT "places.database.replaceOnStartup" // 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 ENV_ALLOW_CORRUPTION "ALLOW_PLACES_DATABASE_TO_LOSE_DATA_AND_BECOME_CORRUPT" // The maximum url length we can store in history. // We do not add to history URLs longer than this value. #define PREF_HISTORY_MAXURLLEN "places.history.maxUrlLength" // This number is mostly a guess based on various facts: // * IE didn't support urls longer than 2083 chars // * Sitemaps protocol used to support a maximum of 2048 chars // * Various SEO guides suggest to not go over 2000 chars // * Various apps/services are known to have issues over 2000 chars // * RFC 2616 - HTTP/1.1 suggests being cautious about depending // on URI lengths above 255 bytes #define PREF_HISTORY_MAXURLLEN_DEFAULT 2000 // Maximum size for the WAL file. It should be small enough since in case of // crashes we could lose all the transactions in the file. But a too small // file could hurt performance. #define DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES 512 #define BYTES_PER_KIBIBYTE 1024 // How much time Sqlite can wait before returning a SQLITE_BUSY error. #define DATABASE_BUSY_TIMEOUT_MS 100 // Old Sync GUID annotation. #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid") // Places string bundle, contains internationalized bookmark root names. #define PLACES_BUNDLE "chrome://places/locale/places.properties" // Livemarks annotations. #define LMANNO_FEEDURI "livemark/feedURI" #define LMANNO_SITEURI "livemark/siteURI" #define MOBILE_ROOT_GUID "mobile______" #define MOBILE_ROOT_ANNO "mobile/bookmarksRoot" // 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" using namespace mozilla; namespace mozilla { namespace places { namespace { //////////////////////////////////////////////////////////////////////////////// //// Helpers /** * Checks whether exists a database backup created not longer than * RECENT_BACKUP_TIME_MICROSEC ago. */ bool hasRecentCorruptDB() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr profDir; NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir)); NS_ENSURE_TRUE(profDir, false); nsCOMPtr entries; profDir->GetDirectoryEntries(getter_AddRefs(entries)); NS_ENSURE_TRUE(entries, false); bool hasMore; while (NS_SUCCEEDED(entries->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr next; entries->GetNext(getter_AddRefs(next)); NS_ENSURE_TRUE(next, false); nsCOMPtr currFile = do_QueryInterface(next); NS_ENSURE_TRUE(currFile, false); nsAutoString leafName; if (NS_SUCCEEDED(currFile->GetLeafName(leafName)) && leafName.Length() >= DATABASE_CORRUPT_FILENAME.Length() && leafName.Find(".corrupt", DATABASE_FILENAME.Length()) != -1) { PRTime lastMod = 0; currFile->GetLastModifiedTime(&lastMod); NS_ENSURE_TRUE(lastMod > 0, false); return (PR_Now() - lastMod) > RECENT_BACKUP_TIME_MICROSEC; } } return false; } /** * Updates sqlite_stat1 table through ANALYZE. * Since also nsPlacesExpiration.js executes ANALYZE, the analyzed tables * must be the same in both components. So ensure they are in sync. * * @param aDBConn * The database connection. */ nsresult updateSQLiteStatistics(mozIStorageConnection* aDBConn) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr analyzePlacesStmt; aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "ANALYZE moz_places" ), getter_AddRefs(analyzePlacesStmt)); NS_ENSURE_STATE(analyzePlacesStmt); nsCOMPtr analyzeBookmarksStmt; aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "ANALYZE moz_bookmarks" ), getter_AddRefs(analyzeBookmarksStmt)); NS_ENSURE_STATE(analyzeBookmarksStmt); nsCOMPtr analyzeVisitsStmt; aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "ANALYZE moz_historyvisits" ), getter_AddRefs(analyzeVisitsStmt)); NS_ENSURE_STATE(analyzeVisitsStmt); nsCOMPtr analyzeInputStmt; aDBConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "ANALYZE moz_inputhistory" ), getter_AddRefs(analyzeInputStmt)); NS_ENSURE_STATE(analyzeInputStmt); mozIStorageBaseStatement *stmts[] = { analyzePlacesStmt, analyzeBookmarksStmt, analyzeVisitsStmt, analyzeInputStmt }; nsCOMPtr ps; (void)aDBConn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr, getter_AddRefs(ps)); return NS_OK; } /** * 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& 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 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; } // This is an unknown journal. MOZ_ASSERT(true); } return JOURNAL_DELETE; } nsresult CreateRoot(nsCOMPtr& aDBConn, const nsCString& aRootName, const nsCString& aGuid, const nsXPIDLString& titleString) { MOZ_ASSERT(NS_IsMainThread()); // The position of the new item in its folder. static int32_t itemPosition = 0; // 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 stmt; nsresult rv = aDBConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO moz_bookmarks " "(type, position, title, dateAdded, lastModified, guid, parent) " "VALUES (:item_type, :item_position, :item_title," ":date_added, :last_modified, :guid," "IFNULL((SELECT id FROM moz_bookmarks WHERE parent = 0), 0))" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) return rv; rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), nsINavBookmarksService::TYPE_FOLDER); if (NS_FAILED(rv)) return rv; rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("item_position"), itemPosition); if (NS_FAILED(rv)) return rv; rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), NS_ConvertUTF16toUTF8(titleString)); if (NS_FAILED(rv)) return rv; rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("date_added"), timestamp); if (NS_FAILED(rv)) return rv; rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("last_modified"), timestamp); if (NS_FAILED(rv)) return rv; rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aGuid); if (NS_FAILED(rv)) return rv; rv = stmt->Execute(); if (NS_FAILED(rv)) return rv; // The 'places' root is a folder containing the other roots. // The first bookmark in a folder has position 0. if (!aRootName.EqualsLiteral("places")) ++itemPosition; 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) { 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 Database::GetProfileChangeTeardownPhase() { nsCOMPtr asyncShutdownSvc = services::GetAsyncShutdown(); MOZ_ASSERT(asyncShutdownSvc); if (NS_WARN_IF(!asyncShutdownSvc)) { return nullptr; } // Consumers of Places should shutdown before us, at profile-change-teardown. nsCOMPtr shutdownPhase; DebugOnly rv = asyncShutdownSvc-> GetProfileChangeTeardown(getter_AddRefs(shutdownPhase)); MOZ_ASSERT(NS_SUCCEEDED(rv)); return shutdownPhase.forget(); } already_AddRefed Database::GetProfileBeforeChangePhase() { nsCOMPtr asyncShutdownSvc = services::GetAsyncShutdown(); MOZ_ASSERT(asyncShutdownSvc); if (NS_WARN_IF(!asyncShutdownSvc)) { return nullptr; } // Consumers of Places should shutdown before us, at profile-change-teardown. nsCOMPtr shutdownPhase; DebugOnly rv = asyncShutdownSvc-> GetProfileBeforeChange(getter_AddRefs(shutdownPhase)); MOZ_ASSERT(NS_SUCCEEDED(rv)); return shutdownPhase.forget(); } Database::~Database() { } bool Database::IsShutdownStarted() const { if (!mConnectionShutdown) { // We have already broken the cycle between `this` and `mConnectionShutdown`. return true; } return mConnectionShutdown->IsStarted(); } already_AddRefed Database::GetAsyncStatement(const nsACString& aQuery) const { if (IsShutdownStarted()) { return nullptr; } MOZ_ASSERT(NS_IsMainThread()); return mMainThreadAsyncStatements.GetCachedStatement(aQuery); } already_AddRefed Database::GetStatement(const nsACString& aQuery) const { if (IsShutdownStarted()) { return nullptr; } if (NS_IsMainThread()) { return mMainThreadStatements.GetCachedStatement(aQuery); } return mAsyncThreadStatements.GetCachedStatement(aQuery); } already_AddRefed Database::GetClientsShutdown() { MOZ_ASSERT(mClientsShutdown); return mClientsShutdown->GetClient(); } // static already_AddRefed Database::GetDatabase() { if (PlacesShutdownBlocker::IsStarted()) { return nullptr; } return GetSingleton(); } nsresult Database::Init() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr storage = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); NS_ENSURE_STATE(storage); // Init the database file and connect to it. bool databaseCreated = false; nsresult rv = InitDatabaseFile(storage, &databaseCreated); if (NS_SUCCEEDED(rv) && databaseCreated) { mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CREATE; } else if (rv == NS_ERROR_FILE_CORRUPTED) { // The database is corrupt, backup and replace it with a new one. mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT; rv = BackupAndReplaceDatabaseFile(storage); // Fallback to catch-all handler, that notifies a database locked failure. } // If the database connection still cannot be opened, it may just be locked // by third parties. Send out a notification and interrupt initialization. if (NS_FAILED(rv)) { RefPtr lockedEvent = new PlacesEvent(TOPIC_DATABASE_LOCKED); (void)NS_DispatchToMainThread(lockedEvent); return 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 = InitSchema(&databaseMigrated); if (NS_FAILED(rv)) { mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_CORRUPT; rv = BackupAndReplaceDatabaseFile(storage); NS_ENSURE_SUCCESS(rv, rv); // Try to initialize the schema again on the new database. rv = InitSchema(&databaseMigrated); NS_ENSURE_SUCCESS(rv, rv); } if (databaseMigrated) { mDatabaseStatus = nsINavHistoryService::DATABASE_STATUS_UPGRADED; } if (mDatabaseStatus != nsINavHistoryService::DATABASE_STATUS_OK) { rv = updateSQLiteStatistics(MainConn()); NS_ENSURE_SUCCESS(rv, rv); } // 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); // Notify we have finished database initialization. // Enqueue the notification, so if we init another service that requires // nsNavHistoryService we don't recursive try to get it. RefPtr completeEvent = new PlacesEvent(TOPIC_PLACES_INIT_COMPLETE); rv = NS_DispatchToMainThread(completeEvent); NS_ENSURE_SUCCESS(rv, rv); // At this point we know the Database object points to a valid connection // and we need to setup async shutdown. { // First of all Places clients should block profile-change-teardown. nsCOMPtr shutdownPhase = GetProfileChangeTeardownPhase(); MOZ_ASSERT(shutdownPhase); if (shutdownPhase) { DebugOnly rv = shutdownPhase->AddBlocker( static_cast(mClientsShutdown.get()), NS_LITERAL_STRING(__FILE__), __LINE__, NS_LITERAL_STRING("")); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } { // Then connection closing should block profile-before-change. nsCOMPtr shutdownPhase = GetProfileBeforeChangePhase(); MOZ_ASSERT(shutdownPhase); if (shutdownPhase) { DebugOnly rv = shutdownPhase->AddBlocker( static_cast(mConnectionShutdown.get()), NS_LITERAL_STRING(__FILE__), __LINE__, NS_LITERAL_STRING("")); MOZ_ASSERT(NS_SUCCEEDED(rv)); } } // Finally observe profile shutdown notifications. nsCOMPtr os = mozilla::services::GetObserverService(); if (os) { (void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true); } return NS_OK; } nsresult Database::InitDatabaseFile(nsCOMPtr& aStorage, bool* aNewDatabaseCreated) { MOZ_ASSERT(NS_IsMainThread()); *aNewDatabaseCreated = false; nsCOMPtr databaseFile; nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(databaseFile)); NS_ENSURE_SUCCESS(rv, rv); rv = databaseFile->Append(DATABASE_FILENAME); NS_ENSURE_SUCCESS(rv, rv); bool databaseFileExists = false; rv = databaseFile->Exists(&databaseFileExists); NS_ENSURE_SUCCESS(rv, rv); if (databaseFileExists && Preferences::GetBool(PREF_FORCE_DATABASE_REPLACEMENT, false)) { // 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); return NS_ERROR_FILE_CORRUPTED; } // 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 = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn)); NS_ENSURE_SUCCESS(rv, rv); *aNewDatabaseCreated = !databaseFileExists; return NS_OK; } nsresult Database::BackupAndReplaceDatabaseFile(nsCOMPtr& aStorage) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr profDir; nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(profDir)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr databaseFile; rv = profDir->Clone(getter_AddRefs(databaseFile)); NS_ENSURE_SUCCESS(rv, rv); rv = databaseFile->Append(DATABASE_FILENAME); NS_ENSURE_SUCCESS(rv, rv); // If we have // 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. if (!hasRecentCorruptDB()) { nsCOMPtr backup; (void)aStorage->BackupDatabaseFile(databaseFile, DATABASE_CORRUPT_FILENAME, 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. { enum eCorruptDBReplaceStage : int8_t { stage_closing = 0, stage_removing, stage_reopening, stage_replaced }; 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::SetBool(PREF_FORCE_DATABASE_REPLACEMENT, true); } }); // Close database connection if open. if (mMainConn) { rv = mMainConn->Close(); NS_ENSURE_SUCCESS(rv, rv); } // Remove the broken database. stage = stage_removing; rv = databaseFile->Remove(false); if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST) { return rv; } // Create a new database file. // Use an unshared connection, it will consume more memory but avoid shared // cache contentions across threads. stage = stage_reopening; rv = aStorage->OpenUnsharedDatabase(databaseFile, getter_AddRefs(mMainConn)); NS_ENSURE_SUCCESS(rv, rv); stage = stage_replaced; } return NS_OK; } nsresult Database::InitSchema(bool* aDatabaseMigrated) { MOZ_ASSERT(NS_IsMainThread()); *aDatabaseMigrated = false; // 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 statement; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( 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_FAILURE); rv = statement->GetInt32(0, &mDBPageSize); NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && mDBPageSize > 0, NS_ERROR_UNEXPECTED); } // Ensure that temp tables are held in memory, not on disk. nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA temp_store = MEMORY")); NS_ENSURE_SUCCESS(rv, 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(mMainConn, JOURNAL_MEMORY); rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA synchronous = OFF")); 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(mMainConn, JOURNAL_WAL)) { // Set the WAL journal size limit. We want it to be small, since in // synchronous = NORMAL mode a crash could cause loss of all the // transactions in the journal. For added safety we will also force // checkpointing at strategic moments. int32_t checkpointPages = static_cast(DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES * 1024 / mDBPageSize); nsAutoCString checkpointPragma("PRAGMA wal_autocheckpoint = "); checkpointPragma.AppendInt(checkpointPages); rv = mMainConn->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(mMainConn, JOURNAL_TRUNCATE); // Set synchronous to FULL to ensure maximum data integrity, even in // case of crashes or unclean shutdowns. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "PRAGMA synchronous = FULL")); 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, especially on // mobile devices, limit its size. // Since exceeding the limit will cause a truncate, allow a slightly // larger limit than DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES to reduce the number // of times it is needed. nsAutoCString journalSizePragma("PRAGMA journal_size_limit = "); journalSizePragma.AppendInt(DATABASE_MAX_WAL_SIZE_IN_KIBIBYTES * 3); (void)mMainConn->ExecuteSimpleSQL(journalSizePragma); // Grow places in |growthIncrementKiB| increments to limit fragmentation on disk. // By default, it's 10 MB. int32_t growthIncrementKiB = Preferences::GetInt(PREF_GROWTH_INCREMENT_KIB, 10 * BYTES_PER_KIBIBYTE); if (growthIncrementKiB > 0) { (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString()); } nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = "); busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS); (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma); // We use our functions during migration, so initialize them now. rv = InitFunctions(); NS_ENSURE_SUCCESS(rv, rv); // Get the database schema version. int32_t currentSchemaVersion; rv = mMainConn->GetSchemaVersion(¤tSchemaVersion); 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; } // We are going to update the database, so everything from now on should be in // a transaction for performances. mozStorageTransaction transaction(mMainConn, false); 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 > 36) { // These versions are not downgradable. return NS_ERROR_FILE_CORRUPTED; } if (currentSchemaVersion < DATABASE_SCHEMA_VERSION) { *aDatabaseMigrated = true; if (currentSchemaVersion < 11) { // These are versions older than Firefox 4 that are not supported // anymore. In this case it's safer to just replace the database. return NS_ERROR_FILE_CORRUPTED; } // Firefox 4 uses schema version 11. // Firefox 8 uses schema version 12. if (currentSchemaVersion < 13) { rv = MigrateV13Up(); NS_ENSURE_SUCCESS(rv, rv); } if (currentSchemaVersion < 15) { rv = MigrateV15Up(); NS_ENSURE_SUCCESS(rv, rv); } if (currentSchemaVersion < 17) { rv = MigrateV17Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 12 uses schema version 17. if (currentSchemaVersion < 18) { rv = MigrateV18Up(); NS_ENSURE_SUCCESS(rv, rv); } if (currentSchemaVersion < 19) { rv = MigrateV19Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 13 uses schema version 19. if (currentSchemaVersion < 20) { rv = MigrateV20Up(); NS_ENSURE_SUCCESS(rv, rv); } if (currentSchemaVersion < 21) { rv = MigrateV21Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 14 uses schema version 21. if (currentSchemaVersion < 22) { rv = MigrateV22Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 22 uses schema version 22. if (currentSchemaVersion < 23) { rv = MigrateV23Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 24 uses schema version 23. if (currentSchemaVersion < 24) { rv = MigrateV24Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 34 uses schema version 24. if (currentSchemaVersion < 25) { rv = MigrateV25Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 36 uses schema version 25. if (currentSchemaVersion < 26) { rv = MigrateV26Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 37 uses schema version 26. if (currentSchemaVersion < 27) { rv = MigrateV27Up(); NS_ENSURE_SUCCESS(rv, rv); } if (currentSchemaVersion < 28) { rv = MigrateV28Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 39 uses schema version 28. if (currentSchemaVersion < 30) { rv = MigrateV30Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 41 uses schema version 30. if (currentSchemaVersion < 31) { rv = MigrateV31Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 48 uses schema version 31. if (currentSchemaVersion < 32) { rv = MigrateV32Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 49 uses schema version 32. if (currentSchemaVersion < 33) { rv = MigrateV33Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 50 uses schema version 33. if (currentSchemaVersion < 34) { rv = MigrateV34Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 51 uses schema version 34. if (currentSchemaVersion < 35) { rv = MigrateV35Up(); NS_ENSURE_SUCCESS(rv, rv); } // Firefox 52 uses schema version 35. // Schema Upgrades must add migration code here. rv = UpdateBookmarkRootTitles(); // We don't want a broken localization to cause us to think // the database is corrupt and needs to be replaced. MOZ_ASSERT(NS_SUCCEEDED(rv)); } } else { // This is a new database, so we have to create all the tables and indices. // 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_FAVICON); 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); // 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_hosts. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS); NS_ENSURE_SUCCESS(rv, rv); // moz_bookmarks. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_BOOKMARKS); 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_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_favicons. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_FAVICONS); 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); // Initialize the bookmark roots in the new DB. rv = CreateBookmarkRoots(); NS_ENSURE_SUCCESS(rv, rv); } // 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); ForceWALCheckpoint(); // 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::CreateBookmarkRoots() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr bundleService = services::GetStringBundleService(); NS_ENSURE_STATE(bundleService); nsCOMPtr bundle; nsresult rv = bundleService->CreateBundle(PLACES_BUNDLE, getter_AddRefs(bundle)); if (NS_FAILED(rv)) return rv; nsXPIDLString rootTitle; // The first root's title is an empty string. rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("places"), NS_LITERAL_CSTRING("root________"), rootTitle); if (NS_FAILED(rv)) return rv; // Fetch the internationalized folder name from the string bundle. rv = bundle->GetStringFromName(u"BookmarksMenuFolderTitle", getter_Copies(rootTitle)); if (NS_FAILED(rv)) return rv; rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("menu"), NS_LITERAL_CSTRING("menu________"), rootTitle); if (NS_FAILED(rv)) return rv; rv = bundle->GetStringFromName(u"BookmarksToolbarFolderTitle", getter_Copies(rootTitle)); if (NS_FAILED(rv)) return rv; rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("toolbar"), NS_LITERAL_CSTRING("toolbar_____"), rootTitle); if (NS_FAILED(rv)) return rv; rv = bundle->GetStringFromName(u"TagsFolderTitle", getter_Copies(rootTitle)); if (NS_FAILED(rv)) return rv; rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("tags"), NS_LITERAL_CSTRING("tags________"), rootTitle); if (NS_FAILED(rv)) return rv; rv = bundle->GetStringFromName(u"OtherBookmarksFolderTitle", getter_Copies(rootTitle)); if (NS_FAILED(rv)) return rv; rv = CreateRoot(mMainConn, NS_LITERAL_CSTRING("unfiled"), NS_LITERAL_CSTRING("unfiled_____"), rootTitle); if (NS_FAILED(rv)) return rv; int64_t mobileRootId = CreateMobileRoot(); if (mobileRootId <= 0) return NS_ERROR_FAILURE; #if DEBUG nsCOMPtr stmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT count(*), sum(position) FROM moz_bookmarks" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) return rv; bool hasResult; rv = stmt->ExecuteStep(&hasResult); if (NS_FAILED(rv)) return rv; MOZ_ASSERT(hasResult); int32_t bookmarkCount = stmt->AsInt32(0); int32_t positionSum = stmt->AsInt32(1); MOZ_ASSERT(bookmarkCount == 6 && positionSum == 10); #endif return NS_OK; } nsresult Database::InitFunctions() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = GetUnreversedHostFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = MatchAutoCompleteFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = CalculateFrecencyFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = GenerateGUIDFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = FixupURLFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = FrecencyNotificationFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = StoreLastInsertedIdFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); rv = HashFunction::create(mMainConn); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::InitTempEntities() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERINSERT_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_HISTORYVISITS_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); // Add the triggers that update the moz_hosts table as necessary. rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERINSERT_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_UPDATEHOSTS_TEMP); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_UPDATEHOSTS_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_FRECENCY_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_PLACES_AFTERUPDATE_TYPED_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERINSERT_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_BOOKMARKS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERDELETE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERINSERT_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_KEYWORDS_FOREIGNCOUNT_AFTERUPDATE_TRIGGER); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::UpdateBookmarkRootTitles() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr bundleService = services::GetStringBundleService(); NS_ENSURE_STATE(bundleService); nsCOMPtr bundle; nsresult rv = bundleService->CreateBundle(PLACES_BUNDLE, getter_AddRefs(bundle)); if (NS_FAILED(rv)) return rv; nsCOMPtr stmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_bookmarks SET title = :new_title WHERE guid = :guid" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) return rv; nsCOMPtr paramsArray; rv = stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); if (NS_FAILED(rv)) return rv; const char *rootGuids[] = { "menu________" , "toolbar_____" , "tags________" , "unfiled_____" , "mobile______" }; const char *titleStringIDs[] = { "BookmarksMenuFolderTitle" , "BookmarksToolbarFolderTitle" , "TagsFolderTitle" , "OtherBookmarksFolderTitle" , "MobileBookmarksFolderTitle" }; for (uint32_t i = 0; i < ArrayLength(rootGuids); ++i) { nsXPIDLString title; rv = bundle->GetStringFromName(NS_ConvertASCIItoUTF16(titleStringIDs[i]).get(), getter_Copies(title)); if (NS_FAILED(rv)) return rv; nsCOMPtr params; rv = paramsArray->NewBindingParams(getter_AddRefs(params)); if (NS_FAILED(rv)) return rv; rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), nsDependentCString(rootGuids[i])); if (NS_FAILED(rv)) return rv; rv = params->BindUTF8StringByName(NS_LITERAL_CSTRING("new_title"), NS_ConvertUTF16toUTF8(title)); if (NS_FAILED(rv)) return rv; rv = paramsArray->AddParams(params); if (NS_FAILED(rv)) return rv; } rv = stmt->BindParameters(paramsArray); if (NS_FAILED(rv)) return rv; nsCOMPtr pendingStmt; rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)); if (NS_FAILED(rv)) return rv; return NS_OK; } nsresult Database::MigrateV13Up() { MOZ_ASSERT(NS_IsMainThread()); // Dynamic containers are no longer supported. nsCOMPtr deleteDynContainersStmt; nsresult rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_bookmarks WHERE type = :item_type"), getter_AddRefs(deleteDynContainersStmt)); rv = deleteDynContainersStmt->BindInt32ByName( NS_LITERAL_CSTRING("item_type"), nsINavBookmarksService::TYPE_DYNAMIC_CONTAINER ); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ps; rv = deleteDynContainersStmt->ExecuteAsync(nullptr, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV15Up() { MOZ_ASSERT(NS_IsMainThread()); // Drop moz_bookmarks_beforedelete_v1_trigger, since it's more expensive than // useful. nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER IF EXISTS moz_bookmarks_beforedelete_v1_trigger" )); NS_ENSURE_SUCCESS(rv, rv); // Remove any orphan keywords. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DELETE FROM moz_keywords " "WHERE NOT EXISTS ( " "SELECT id " "FROM moz_bookmarks " "WHERE keyword_id = moz_keywords.id " ")" )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV17Up() { MOZ_ASSERT(NS_IsMainThread()); bool tableExists = false; nsresult rv = mMainConn->TableExists(NS_LITERAL_CSTRING("moz_hosts"), &tableExists); NS_ENSURE_SUCCESS(rv, rv); if (!tableExists) { // For anyone who used in-development versions of this autocomplete, // drop the old tables and its indexes. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX IF EXISTS moz_hostnames_frecencyindex" )); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE IF EXISTS moz_hostnames" )); NS_ENSURE_SUCCESS(rv, rv); // Add the moz_hosts table so we can get hostnames for URL autocomplete. rv = mMainConn->ExecuteSimpleSQL(CREATE_MOZ_HOSTS); NS_ENSURE_SUCCESS(rv, rv); } // Fill the moz_hosts table with all the domains in moz_places. nsCOMPtr fillHostsStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO moz_hosts (host, frecency) " "SELECT fixup_url(get_unreversed_host(h.rev_host)) AS host, " "(SELECT MAX(frecency) FROM moz_places " "WHERE rev_host = h.rev_host " "OR rev_host = h.rev_host || 'www.' " ") AS frecency " "FROM moz_places h " "WHERE LENGTH(h.rev_host) > 1 " "GROUP BY h.rev_host" ), getter_AddRefs(fillHostsStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ps; rv = fillHostsStmt->ExecuteAsync(nullptr, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV18Up() { MOZ_ASSERT(NS_IsMainThread()); // moz_hosts should distinguish on typed entries. // Check if the profile already has a typed column. nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT typed FROM moz_hosts" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD COLUMN typed NOT NULL DEFAULT 0" )); NS_ENSURE_SUCCESS(rv, rv); } // With the addition of the typed column the covering index loses its // advantages. On the other side querying on host and (optionally) typed // largely restricts the number of results, making scans decently fast. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX IF EXISTS moz_hosts_frecencyhostindex" )); NS_ENSURE_SUCCESS(rv, rv); // Update typed data. nsCOMPtr updateTypedStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_hosts SET typed = 1 WHERE host IN ( " "SELECT fixup_url(get_unreversed_host(rev_host)) " "FROM moz_places WHERE typed = 1 " ") " ), getter_AddRefs(updateTypedStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ps; rv = updateTypedStmt->ExecuteAsync(nullptr, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV19Up() { MOZ_ASSERT(NS_IsMainThread()); // Livemarks children are no longer bookmarks. // Remove all children of folders annotated as livemarks. nsCOMPtr deleteLivemarksChildrenStmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_bookmarks WHERE parent IN(" "SELECT b.id FROM moz_bookmarks b " "JOIN moz_items_annos a ON a.item_id = b.id " "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " "WHERE b.type = :item_type AND n.name = :anno_name " ")" ), getter_AddRefs(deleteLivemarksChildrenStmt)); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksChildrenStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(LMANNO_FEEDURI) ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksChildrenStmt->BindInt32ByName( NS_LITERAL_CSTRING("item_type"), nsINavBookmarksService::TYPE_FOLDER ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksChildrenStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); // Clear obsolete livemark prefs. (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_seconds"); (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_limit_count"); (void)Preferences::ClearUser("browser.bookmarks.livemark_refresh_delay_time"); // Remove the old status annotations. nsCOMPtr deleteLivemarksAnnosStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_items_annos WHERE anno_attribute_id IN(" "SELECT id FROM moz_anno_attributes " "WHERE name IN (:anno_loading, :anno_loadfailed, :anno_expiration) " ")" ), getter_AddRefs(deleteLivemarksAnnosStmt)); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_loading"), NS_LITERAL_CSTRING("livemark/loading") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_loadfailed"), NS_LITERAL_CSTRING("livemark/loadfailed") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_expiration"), NS_LITERAL_CSTRING("livemark/expiration") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); // Remove orphan annotation names. rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_anno_attributes " "WHERE name IN (:anno_loading, :anno_loadfailed, :anno_expiration) " ), getter_AddRefs(deleteLivemarksAnnosStmt)); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_loading"), NS_LITERAL_CSTRING("livemark/loading") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_loadfailed"), NS_LITERAL_CSTRING("livemark/loadfailed") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_expiration"), NS_LITERAL_CSTRING("livemark/expiration") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteLivemarksAnnosStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV20Up() { MOZ_ASSERT(NS_IsMainThread()); // Remove obsolete bookmark GUID annotations. nsCOMPtr deleteOldBookmarkGUIDAnnosStmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_items_annos WHERE anno_attribute_id = (" "SELECT id FROM moz_anno_attributes " "WHERE name = :anno_guid" ")" ), getter_AddRefs(deleteOldBookmarkGUIDAnnosStmt)); NS_ENSURE_SUCCESS(rv, rv); rv = deleteOldBookmarkGUIDAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_guid"), NS_LITERAL_CSTRING("placesInternal/GUID") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteOldBookmarkGUIDAnnosStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); // Remove the orphan annotation name. rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_anno_attributes " "WHERE name = :anno_guid" ), getter_AddRefs(deleteOldBookmarkGUIDAnnosStmt)); NS_ENSURE_SUCCESS(rv, rv); rv = deleteOldBookmarkGUIDAnnosStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_guid"), NS_LITERAL_CSTRING("placesInternal/GUID") ); NS_ENSURE_SUCCESS(rv, rv); rv = deleteOldBookmarkGUIDAnnosStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV21Up() { MOZ_ASSERT(NS_IsMainThread()); // Add a prefix column to moz_hosts. nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT prefix FROM moz_hosts" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_hosts ADD COLUMN prefix" )); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult Database::MigrateV22Up() { MOZ_ASSERT(NS_IsMainThread()); // Reset all session IDs to 0 since we don't support them anymore. // We don't set them to NULL to avoid breaking downgrades. nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_historyvisits SET session = 0" )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV23Up() { MOZ_ASSERT(NS_IsMainThread()); // Recalculate hosts prefixes. nsCOMPtr updatePrefixesStmt; nsresult rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_hosts SET prefix = ( " HOSTS_PREFIX_PRIORITY_FRAGMENT ") " ), getter_AddRefs(updatePrefixesStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr ps; rv = updatePrefixesStmt->ExecuteAsync(nullptr, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV24Up() { MOZ_ASSERT(NS_IsMainThread()); // Add a foreign_count column to moz_places nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT foreign_count FROM moz_places" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_places ADD COLUMN foreign_count INTEGER DEFAULT 0 NOT NULL")); NS_ENSURE_SUCCESS(rv, rv); } // Adjust counts for all the rows nsCOMPtr updateStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_places SET foreign_count = " "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) " ), getter_AddRefs(updateStmt)); NS_ENSURE_SUCCESS(rv, rv); mozStorageStatementScoper updateScoper(updateStmt); rv = updateStmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV25Up() { MOZ_ASSERT(NS_IsMainThread()); // Change bookmark roots GUIDs to constant values. // If moz_bookmarks_roots doesn't exist anymore, it's because we finally have // been able to remove it. In such a case, we already assigned constant GUIDs // to the roots and we can skip this migration. { nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT root_name FROM moz_bookmarks_roots" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { return NS_OK; } } nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_bookmarks SET guid = :guid " "WHERE id = (SELECT folder_id FROM moz_bookmarks_roots WHERE root_name = :name) " ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); const char *rootNames[] = { "places", "menu", "toolbar", "tags", "unfiled" }; const char *rootGuids[] = { "root________" , "menu________" , "toolbar_____" , "tags________" , "unfiled_____" }; for (uint32_t i = 0; i < ArrayLength(rootNames); ++i) { // Since this is using the synchronous API, we cannot use // a BindingParamsArray. mozStorageStatementScoper scoper(stmt); rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), nsDependentCString(rootNames[i])); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), nsDependentCString(rootGuids[i])); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } nsresult Database::MigrateV26Up() { MOZ_ASSERT(NS_IsMainThread()); // Round down dateAdded and lastModified values to milliseconds precision. nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_bookmarks SET dateAdded = dateAdded - dateAdded % 1000, " " lastModified = lastModified - lastModified % 1000")); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV27Up() { MOZ_ASSERT(NS_IsMainThread()); // Change keywords store, moving their relation from bookmarks to urls. nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT place_id FROM moz_keywords" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { // Even if these 2 columns have a unique constraint, we allow NULL values // for backwards compatibility. NULL never breaks a unique constraint. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_keywords ADD COLUMN place_id INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_keywords ADD COLUMN post_data TEXT")); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_KEYWORDS_PLACEPOSTDATA); NS_ENSURE_SUCCESS(rv, rv); } // Associate keywords with uris. A keyword could be associated to multiple // bookmarks uris, or multiple keywords could be associated to the same uri. // The new system only allows multiple uris per keyword, provided they have // a different post_data value. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "INSERT OR REPLACE INTO moz_keywords (id, keyword, place_id, post_data) " "SELECT k.id, k.keyword, h.id, MAX(a.content) " "FROM moz_places h " "JOIN moz_bookmarks b ON b.fk = h.id " "JOIN moz_keywords k ON k.id = b.keyword_id " "LEFT JOIN moz_items_annos a ON a.item_id = b.id " "AND a.anno_attribute_id = (SELECT id FROM moz_anno_attributes " "WHERE name = 'bookmarkProperties/POSTData') " "WHERE k.place_id ISNULL " "GROUP BY keyword")); NS_ENSURE_SUCCESS(rv, rv); // Remove any keyword that points to a non-existing place id. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DELETE FROM moz_keywords " "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = moz_keywords.place_id)")); NS_ENSURE_SUCCESS(rv, rv); rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_bookmarks SET keyword_id = NULL " "WHERE NOT EXISTS (SELECT 1 FROM moz_keywords WHERE id = moz_bookmarks.keyword_id)")); NS_ENSURE_SUCCESS(rv, rv); // Adjust foreign_count for all the rows. rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_places SET foreign_count = " "(SELECT count(*) FROM moz_bookmarks WHERE fk = moz_places.id) + " "(SELECT count(*) FROM moz_keywords WHERE place_id = moz_places.id) " )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV28Up() { MOZ_ASSERT(NS_IsMainThread()); // v27 migration was bogus and set some unrelated annotations as post_data for // keywords having an annotated bookmark. // The current v27 migration function is fixed, but we still need to handle // users that hit the bogus version. Since we can't distinguish, we'll just // set again all of the post data. DebugOnly rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_keywords " "SET post_data = ( " "SELECT content FROM moz_items_annos a " "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " "JOIN moz_bookmarks b on b.id = a.item_id " "WHERE n.name = 'bookmarkProperties/POSTData' " "AND b.keyword_id = moz_keywords.id " "ORDER BY b.lastModified DESC " "LIMIT 1 " ") " "WHERE EXISTS(SELECT 1 FROM moz_bookmarks WHERE keyword_id = moz_keywords.id) " )); // In case the update fails a constraint, we don't want to throw away the // whole database for just a few keywords. In rare cases the user might have // to recreate them. Though, at this point, there shouldn't be 2 keywords // pointing to the same url and post data, cause the previous migration step // removed them. MOZ_ASSERT(NS_SUCCEEDED(rv)); return NS_OK; } nsresult Database::MigrateV30Up() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX IF EXISTS moz_favicons_guid_uniqueindex" )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV31Up() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TABLE IF EXISTS moz_bookmarks_roots" )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV32Up() { MOZ_ASSERT(NS_IsMainThread()); // Remove some old and no more used Places preferences that may be confusing // for the user. mozilla::Unused << Preferences::ClearUser("places.history.expiration.transient_optimal_database_size"); mozilla::Unused << Preferences::ClearUser("places.last_vacuum"); mozilla::Unused << Preferences::ClearUser("browser.history_expire_sites"); mozilla::Unused << Preferences::ClearUser("browser.history_expire_days.mirror"); mozilla::Unused << Preferences::ClearUser("browser.history_expire_days_min"); // For performance reasons we want to remove too long urls from history. // We cannot use the moz_places triggers here, cause they are defined only // after the schema migration. Thus we need to collect the hosts that need to // be updated first. nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TEMP TABLE moz_migrate_v32_temp (" "host TEXT PRIMARY KEY " ") WITHOUT ROWID " )); NS_ENSURE_SUCCESS(rv, rv); { nsCOMPtr stmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO moz_migrate_v32_temp (host) " "SELECT fixup_url(get_unreversed_host(rev_host)) " "FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0" ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); mozStorageStatementScoper scoper(stmt); rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength()); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); } // Now remove the pages with a long url. { nsCOMPtr stmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_places WHERE LENGTH(url) > :maxlen AND foreign_count = 0" ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); mozStorageStatementScoper scoper(stmt); rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("maxlen"), MaxUrlLength()); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); } // Expire orphan visits and update moz_hosts. // These may be a bit more expensive and are not critical for the DB // functionality, so we execute them asynchronously. nsCOMPtr expireOrphansStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_historyvisits " "WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = place_id)" ), getter_AddRefs(expireOrphansStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr deleteHostsStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_hosts " "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) " "AND NOT EXISTS(" "SELECT 1 FROM moz_places " "WHERE rev_host = get_unreversed_host(host || '.') || '.' " "OR rev_host = get_unreversed_host(host || '.') || '.www.' " "); " ), getter_AddRefs(deleteHostsStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr updateHostsStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "UPDATE moz_hosts " "SET prefix = (" HOSTS_PREFIX_PRIORITY_FRAGMENT ") " "WHERE host IN (SELECT host FROM moz_migrate_v32_temp) " ), getter_AddRefs(updateHostsStmt)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr dropTableStmt; rv = mMainConn->CreateAsyncStatement(NS_LITERAL_CSTRING( "DROP TABLE IF EXISTS moz_migrate_v32_temp" ), getter_AddRefs(dropTableStmt)); NS_ENSURE_SUCCESS(rv, rv); mozIStorageBaseStatement *stmts[] = { expireOrphansStmt, deleteHostsStmt, updateHostsStmt, dropTableStmt }; nsCOMPtr ps; rv = mMainConn->ExecuteAsync(stmts, ArrayLength(stmts), nullptr, getter_AddRefs(ps)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV33Up() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP INDEX IF EXISTS moz_places_url_uniqueindex" )); NS_ENSURE_SUCCESS(rv, rv); // Add an url_hash column to moz_places. nsCOMPtr stmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT url_hash FROM moz_places" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) { rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_places ADD COLUMN url_hash INTEGER DEFAULT 0 NOT NULL" )); NS_ENSURE_SUCCESS(rv, rv); } rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "UPDATE moz_places SET url_hash = hash(url) WHERE url_hash = 0" )); NS_ENSURE_SUCCESS(rv, rv); // Create an index on url_hash. rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_URL_HASH); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV34Up() { MOZ_ASSERT(NS_IsMainThread()); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DELETE FROM moz_keywords WHERE id IN ( " "SELECT id FROM moz_keywords k " "WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) " ")" )); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult Database::MigrateV35Up() { MOZ_ASSERT(NS_IsMainThread()); int64_t mobileRootId = CreateMobileRoot(); if (mobileRootId <= 0) { // Either the schema is broken or there isn't any root. The latter can // happen if a consumer, for example Thunderbird, never used bookmarks. // If there are no roots, this migration should not run. nsCOMPtr checkRootsStmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id FROM moz_bookmarks WHERE parent = 0" ), getter_AddRefs(checkRootsStmt)); NS_ENSURE_SUCCESS(rv, rv); mozStorageStatementScoper scoper(checkRootsStmt); bool hasResult = false; rv = checkRootsStmt->ExecuteStep(&hasResult); if (NS_SUCCEEDED(rv) && !hasResult) { return NS_OK; } return NS_ERROR_FAILURE; } // At this point, we should have no more than two folders with the mobile // bookmarks anno: the new root, and the old folder if one exists. If, for // some reason, we have multiple folders with the anno, we append their // children to the new root. nsTArray folderIds; nsresult rv = GetItemsWithAnno(NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO), nsINavBookmarksService::TYPE_FOLDER, folderIds); if (NS_FAILED(rv)) return rv; for (uint32_t i = 0; i < folderIds.Length(); ++i) { if (folderIds[i] == mobileRootId) { // Ignore the new mobile root. We'll remove this anno from the root in // bug 1306445. continue; } // Append the folder's children to the new root. nsCOMPtr moveStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_bookmarks " "SET parent = :root_id, " "position = position + IFNULL(" "(SELECT MAX(position) + 1 FROM moz_bookmarks " "WHERE parent = :root_id), 0)" "WHERE parent = :folder_id" ), getter_AddRefs(moveStmt)); if (NS_FAILED(rv)) return rv; mozStorageStatementScoper moveScoper(moveStmt); rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"), mobileRootId); if (NS_FAILED(rv)) return rv; rv = moveStmt->BindInt64ByName(NS_LITERAL_CSTRING("folder_id"), folderIds[i]); if (NS_FAILED(rv)) return rv; rv = moveStmt->Execute(); if (NS_FAILED(rv)) return rv; // Delete the old folder. rv = DeleteBookmarkItem(folderIds[i]); if (NS_FAILED(rv)) return rv; } return NS_OK; } nsresult Database::GetItemsWithAnno(const nsACString& aAnnoName, int32_t aItemType, nsTArray& aItemIds) { nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT b.id FROM moz_items_annos a " "JOIN moz_anno_attributes n ON n.id = a.anno_attribute_id " "JOIN moz_bookmarks b ON b.id = a.item_id " "WHERE n.name = :anno_name AND " "b.type = :item_type" ), getter_AddRefs(stmt)); if (NS_FAILED(rv)) return rv; mozStorageStatementScoper scoper(stmt); rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("anno_name"), aAnnoName); if (NS_FAILED(rv)) return rv; rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("item_type"), aItemType); if (NS_FAILED(rv)) return rv; bool hasMore = false; while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) { int64_t itemId; rv = stmt->GetInt64(0, &itemId); if (NS_FAILED(rv)) return rv; aItemIds.AppendElement(itemId); } return NS_OK; } nsresult Database::DeleteBookmarkItem(int32_t aItemId) { // Delete the old bookmark. nsCOMPtr deleteStmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_bookmarks WHERE id = :item_id" ), getter_AddRefs(deleteStmt)); if (NS_FAILED(rv)) return rv; mozStorageStatementScoper deleteScoper(deleteStmt); rv = deleteStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId); if (NS_FAILED(rv)) return rv; rv = deleteStmt->Execute(); if (NS_FAILED(rv)) return rv; // Clean up orphan annotations. nsCOMPtr removeAnnosStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_items_annos WHERE item_id = :item_id" ), getter_AddRefs(removeAnnosStmt)); if (NS_FAILED(rv)) return rv; mozStorageStatementScoper removeAnnosScoper(removeAnnosStmt); rv = removeAnnosStmt->BindInt64ByName(NS_LITERAL_CSTRING("item_id"), aItemId); if (NS_FAILED(rv)) return rv; rv = removeAnnosStmt->Execute(); if (NS_FAILED(rv)) return rv; return NS_OK; } int64_t Database::CreateMobileRoot() { MOZ_ASSERT(NS_IsMainThread()); // Create the mobile root, ignoring conflicts if one already exists (for // example, if the user downgraded to an earlier release channel). nsCOMPtr createStmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO moz_bookmarks " "(type, title, dateAdded, lastModified, guid, position, parent) " "SELECT :item_type, :item_title, :timestamp, :timestamp, :guid, " "(SELECT COUNT(*) FROM moz_bookmarks p WHERE p.parent = b.id), b.id " "FROM moz_bookmarks b WHERE b.parent = 0" ), getter_AddRefs(createStmt)); if (NS_FAILED(rv)) return -1; mozStorageStatementScoper createScoper(createStmt); rv = createStmt->BindInt32ByName(NS_LITERAL_CSTRING("item_type"), nsINavBookmarksService::TYPE_FOLDER); if (NS_FAILED(rv)) return -1; rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("item_title"), NS_LITERAL_CSTRING(MOBILE_ROOT_TITLE)); if (NS_FAILED(rv)) return -1; rv = createStmt->BindInt64ByName(NS_LITERAL_CSTRING("timestamp"), RoundedPRNow()); if (NS_FAILED(rv)) return -1; rv = createStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), NS_LITERAL_CSTRING(MOBILE_ROOT_GUID)); if (NS_FAILED(rv)) return -1; rv = createStmt->Execute(); if (NS_FAILED(rv)) return -1; // Find the mobile root ID. We can't use the last inserted ID because the // root might already exist, and we ignore on conflict. nsCOMPtr findIdStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id FROM moz_bookmarks WHERE guid = :guid" ), getter_AddRefs(findIdStmt)); if (NS_FAILED(rv)) return -1; mozStorageStatementScoper findIdScoper(findIdStmt); rv = findIdStmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), NS_LITERAL_CSTRING(MOBILE_ROOT_GUID)); if (NS_FAILED(rv)) return -1; bool hasResult = false; rv = findIdStmt->ExecuteStep(&hasResult); if (NS_FAILED(rv) || !hasResult) return -1; int64_t rootId; rv = findIdStmt->GetInt64(0, &rootId); if (NS_FAILED(rv)) return -1; // Set the mobile bookmarks anno on the new root, so that Sync code on an // older channel can still find it in case of a downgrade. This can be // removed in bug 1306445. nsCOMPtr addAnnoNameStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO moz_anno_attributes (name) VALUES (:anno_name)" ), getter_AddRefs(addAnnoNameStmt)); if (NS_FAILED(rv)) return -1; mozStorageStatementScoper addAnnoNameScoper(addAnnoNameStmt); rv = addAnnoNameStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO)); if (NS_FAILED(rv)) return -1; rv = addAnnoNameStmt->Execute(); if (NS_FAILED(rv)) return -1; nsCOMPtr addAnnoStmt; rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT OR IGNORE INTO moz_items_annos " "(id, item_id, anno_attribute_id, content, flags, " "expiration, type, dateAdded, lastModified) " "SELECT " "(SELECT a.id FROM moz_items_annos a " "WHERE a.anno_attribute_id = n.id AND " "a.item_id = :root_id), " ":root_id, n.id, 1, 0, :expiration, :type, :timestamp, :timestamp " "FROM moz_anno_attributes n WHERE name = :anno_name" ), getter_AddRefs(addAnnoStmt)); if (NS_FAILED(rv)) return -1; mozStorageStatementScoper addAnnoScoper(addAnnoStmt); rv = addAnnoStmt->BindInt64ByName(NS_LITERAL_CSTRING("root_id"), rootId); if (NS_FAILED(rv)) return -1; rv = addAnnoStmt->BindUTF8StringByName( NS_LITERAL_CSTRING("anno_name"), NS_LITERAL_CSTRING(MOBILE_ROOT_ANNO)); if (NS_FAILED(rv)) return -1; rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("expiration"), nsIAnnotationService::EXPIRE_NEVER); if (NS_FAILED(rv)) return -1; rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("type"), nsIAnnotationService::TYPE_INT32); if (NS_FAILED(rv)) return -1; rv = addAnnoStmt->BindInt32ByName(NS_LITERAL_CSTRING("timestamp"), RoundedPRNow()); if (NS_FAILED(rv)) return -1; rv = addAnnoStmt->Execute(); if (NS_FAILED(rv)) return -1; return rootId; } void Database::Shutdown() { // As the last step in the shutdown path, finalize the database handle. MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(!mClosed); // Break cycles with the shutdown blockers. mClientsShutdown = nullptr; nsCOMPtr connectionShutdown = mConnectionShutdown.forget(); if (!mMainConn) { // The connection has never been initialized. Just mark it as closed. mClosed = true; (void)connectionShutdown->Complete(NS_OK, nullptr); return; } #ifdef DEBUG { // Sanity check for missing guids. bool haveNullGuids = false; nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT 1 " "FROM moz_places " "WHERE guid IS NULL " ), getter_AddRefs(stmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = stmt->ExecuteStep(&haveNullGuids); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(!haveNullGuids, "Found a page without a GUID!"); rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT 1 " "FROM moz_bookmarks " "WHERE guid IS NULL " ), getter_AddRefs(stmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = stmt->ExecuteStep(&haveNullGuids); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(!haveNullGuids, "Found a bookmark without a GUID!"); } { // Sanity check for unrounded dateAdded and lastModified values (bug // 1107308). bool hasUnroundedDates = false; nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT 1 " "FROM moz_bookmarks " "WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1" ), getter_AddRefs(stmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = stmt->ExecuteStep(&hasUnroundedDates); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(!hasUnroundedDates, "Found unrounded dates!"); } { // Sanity check url_hash bool hasNullHash = false; nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT 1 FROM moz_places WHERE url_hash = 0" ), getter_AddRefs(stmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = stmt->ExecuteStep(&hasNullHash); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(!hasNullHash, "Found a place without a hash!"); } { // Sanity check unique urls bool hasDupeUrls = false; nsCOMPtr stmt; nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT 1 FROM moz_places GROUP BY url HAVING count(*) > 1 " ), getter_AddRefs(stmt)); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = stmt->ExecuteStep(&hasDupeUrls); MOZ_ASSERT(NS_SUCCEEDED(rv)); MOZ_ASSERT(!hasDupeUrls, "Found a duplicate url!"); } #endif mMainThreadStatements.FinalizeStatements(); mMainThreadAsyncStatements.FinalizeStatements(); RefPtr< FinalizeStatementCacheProxy > event = new FinalizeStatementCacheProxy( mAsyncThreadStatements, NS_ISUPPORTS_CAST(nsIObserver*, this) ); DispatchToAsyncThread(event); mClosed = true; (void)mMainConn->AsyncClose(connectionShutdown); } //////////////////////////////////////////////////////////////////////////////// //// nsIObserver NS_IMETHODIMP Database::Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) { MOZ_ASSERT(NS_IsMainThread()); if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0) { // Tests simulating shutdown may cause multiple notifications. if (IsShutdownStarted()) { return NS_OK; } nsCOMPtr os = services::GetObserverService(); NS_ENSURE_STATE(os); // If shutdown happens in the same mainthread loop as init, observers could // handle the places-init-complete notification after xpcom-shutdown, when // the connection does not exist anymore. Removing those observers would // be less expensive but may cause their RemoveObserver calls to throw. // Thus notify the topic now, so they stop listening for it. nsCOMPtr e; if (NS_SUCCEEDED(os->EnumerateObservers(TOPIC_PLACES_INIT_COMPLETE, getter_AddRefs(e))) && e) { bool hasMore = false; while (NS_SUCCEEDED(e->HasMoreElements(&hasMore)) && hasMore) { nsCOMPtr supports; if (NS_SUCCEEDED(e->GetNext(getter_AddRefs(supports)))) { nsCOMPtr observer = do_QueryInterface(supports); (void)observer->Observe(observer, TOPIC_PLACES_INIT_COMPLETE, nullptr); } } } // Notify all Places users that we are about to shutdown. (void)os->NotifyObservers(nullptr, TOPIC_PLACES_SHUTDOWN, nullptr); } else if (strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) { // This notification is (and must be) only used by tests that are trying // to simulate Places shutdown out of the normal shutdown path. // Tests simulating shutdown may cause re-entrance. if (IsShutdownStarted()) { return NS_OK; } // We are simulating a shutdown, so invoke the shutdown blockers, // wait for them, then proceed with connection shutdown. // Since we are already going through shutdown, but it's not the real one, // we won't need to block the real one anymore, so we can unblock it. { nsCOMPtr shutdownPhase = GetProfileChangeTeardownPhase(); if (shutdownPhase) { shutdownPhase->RemoveBlocker(mClientsShutdown.get()); } (void)mClientsShutdown->BlockShutdown(nullptr); } // Spin the events loop until the clients are done. // Note, this is just for tests, specifically test_clearHistory_shutdown.js while (mClientsShutdown->State() != PlacesShutdownBlocker::States::RECEIVED_DONE) { (void)NS_ProcessNextEvent(); } { nsCOMPtr shutdownPhase = GetProfileBeforeChangePhase(); if (shutdownPhase) { shutdownPhase->RemoveBlocker(mConnectionShutdown.get()); } (void)mConnectionShutdown->BlockShutdown(nullptr); } } return NS_OK; } uint32_t Database::MaxUrlLength() { MOZ_ASSERT(NS_IsMainThread()); if (!mMaxUrlLength) { mMaxUrlLength = Preferences::GetInt(PREF_HISTORY_MAXURLLEN, PREF_HISTORY_MAXURLLEN_DEFAULT); if (mMaxUrlLength < 255 || mMaxUrlLength > INT32_MAX) { mMaxUrlLength = PREF_HISTORY_MAXURLLEN_DEFAULT; } } return mMaxUrlLength; } } // namespace places } // namespace mozilla