/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "base/basictypes.h" #include "IDBFactory.h" #include "nsIFile.h" #include "nsIPrincipal.h" #include "nsIScriptContext.h" #include "nsIXPConnect.h" #include "nsIXPCScriptable.h" #include #include "mozilla/dom/ContentParent.h" #include "mozilla/dom/ContentChild.h" #include "mozilla/dom/IDBFactoryBinding.h" #include "mozilla/dom/PBrowserChild.h" #include "mozilla/dom/quota/OriginOrPatternString.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/dom/TabChild.h" #include "mozilla/storage.h" #include "nsComponentManagerUtils.h" #include "nsIScriptSecurityManager.h" #include "nsCharSeparatedTokenizer.h" #include "nsContentUtils.h" #include "nsCxPusher.h" #include "nsDOMClassInfoID.h" #include "nsGlobalWindow.h" #include "nsHashKeys.h" #include "nsPIDOMWindow.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" #include "nsXPCOMCID.h" #include "AsyncConnectionHelper.h" #include "CheckPermissionsHelper.h" #include "DatabaseInfo.h" #include "IDBDatabase.h" #include "IDBEvents.h" #include "IDBKeyRange.h" #include "IndexedDatabaseManager.h" #include "Key.h" #include "ProfilerHelpers.h" #include "ipc/IndexedDBChild.h" USING_INDEXEDDB_NAMESPACE USING_QUOTA_NAMESPACE using mozilla::dom::ContentChild; using mozilla::dom::ContentParent; using mozilla::dom::NonNull; using mozilla::dom::Optional; using mozilla::dom::TabChild; using mozilla::ErrorResult; namespace { struct ObjectStoreInfoMap { ObjectStoreInfoMap() : id(INT64_MIN), info(nullptr) { } int64_t id; ObjectStoreInfo* info; }; } // anonymous namespace IDBFactory::IDBFactory() : mOwningObject(nullptr), mActorChild(nullptr), mActorParent(nullptr), mContentParent(nullptr), mRootedOwningObject(false) { SetIsDOMBinding(); } IDBFactory::~IDBFactory() { NS_ASSERTION(!mActorParent, "Actor parent owns us, how can we be dying?!"); if (mActorChild) { NS_ASSERTION(!IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); mActorChild->Send__delete__(mActorChild); NS_ASSERTION(!mActorChild, "Should have cleared in Send__delete__!"); } if (mRootedOwningObject) { mOwningObject = nullptr; NS_DROP_JS_OBJECTS(this, IDBFactory); } } // static nsresult IDBFactory::Create(nsPIDOMWindow* aWindow, const nsACString& aASCIIOrigin, ContentParent* aContentParent, IDBFactory** aFactory) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aASCIIOrigin.IsEmpty() || nsContentUtils::IsCallerChrome(), "Non-chrome may not supply their own origin!"); NS_ENSURE_TRUE(aWindow, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); if (aWindow->IsOuterWindow()) { aWindow = aWindow->GetCurrentInnerWindow(); NS_ENSURE_TRUE(aWindow, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } // Make sure that the manager is up before we do anything here since lots of // decisions depend on which process we're running in. indexedDB::IndexedDatabaseManager* mgr = indexedDB::IndexedDatabaseManager::GetOrCreate(); NS_ENSURE_TRUE(mgr, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); nsresult rv; nsCString origin(aASCIIOrigin); if (origin.IsEmpty()) { rv = QuotaManager::GetASCIIOriginFromWindow(aWindow, origin); if (NS_FAILED(rv)) { // Not allowed. *aFactory = nullptr; return NS_OK; } } nsRefPtr factory = new IDBFactory(); factory->mASCIIOrigin = origin; factory->mWindow = aWindow; factory->mContentParent = aContentParent; if (!IndexedDatabaseManager::IsMainProcess()) { TabChild* tabChild = GetTabChildFrom(aWindow); NS_ENSURE_TRUE(tabChild, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); IndexedDBChild* actor = new IndexedDBChild(origin); bool allowed; tabChild->SendPIndexedDBConstructor(actor, origin, &allowed); if (!allowed) { actor->Send__delete__(actor); *aFactory = nullptr; return NS_OK; } actor->SetFactory(factory); } factory.forget(aFactory); return NS_OK; } // static nsresult IDBFactory::Create(JSContext* aCx, JS::Handle aOwningObject, ContentParent* aContentParent, IDBFactory** aFactory) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aCx, "Null context!"); NS_ASSERTION(aOwningObject, "Null object!"); NS_ASSERTION(JS_GetGlobalForObject(aCx, aOwningObject) == aOwningObject, "Not a global object!"); NS_ASSERTION(nsContentUtils::IsCallerChrome(), "Only for chrome!"); nsCString origin; nsresult rv = QuotaManager::GetASCIIOriginFromWindow(nullptr, origin); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); nsRefPtr factory = new IDBFactory(); factory->mASCIIOrigin = origin; factory->mOwningObject = aOwningObject; factory->mContentParent = aContentParent; if (!IndexedDatabaseManager::IsMainProcess()) { ContentChild* contentChild = ContentChild::GetSingleton(); NS_ENSURE_TRUE(contentChild, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); IndexedDBChild* actor = new IndexedDBChild(origin); contentChild->SendPIndexedDBConstructor(actor); actor->SetFactory(factory); } factory.forget(aFactory); return NS_OK; } // static nsresult IDBFactory::Create(ContentParent* aContentParent, IDBFactory** aFactory) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); NS_ASSERTION(nsContentUtils::IsCallerChrome(), "Only for chrome!"); NS_ASSERTION(aContentParent, "Null ContentParent!"); NS_ASSERTION(!nsContentUtils::GetCurrentJSContext(), "Should be called from C++"); nsCOMPtr principal = do_CreateInstance("@mozilla.org/nullprincipal;1"); NS_ENSURE_TRUE(principal, NS_ERROR_FAILURE); AutoSafeJSContext cx; nsIXPConnect* xpc = nsContentUtils::XPConnect(); NS_ASSERTION(xpc, "This should never be null!"); nsCOMPtr globalHolder; nsresult rv = xpc->CreateSandbox(cx, principal, getter_AddRefs(globalHolder)); NS_ENSURE_SUCCESS(rv, rv); JS::Rooted global(cx, globalHolder->GetJSObject()); NS_ENSURE_STATE(global); // The CreateSandbox call returns a proxy to the actual sandbox object. We // don't need a proxy here. global = js::UncheckedUnwrap(global); JSAutoCompartment ac(cx, global); nsRefPtr factory; rv = Create(cx, global, aContentParent, getter_AddRefs(factory)); NS_ENSURE_SUCCESS(rv, rv); NS_HOLD_JS_OBJECTS(factory, IDBFactory); factory->mRootedOwningObject = true; factory.forget(aFactory); return NS_OK; } // static already_AddRefed IDBFactory::GetDatabaseFileURL(nsIFile* aDatabaseFile, const nsACString& aOrigin) { nsCOMPtr uri; nsresult rv = NS_NewFileURI(getter_AddRefs(uri), aDatabaseFile); NS_ENSURE_SUCCESS(rv, nullptr); nsCOMPtr fileUrl = do_QueryInterface(uri); NS_ASSERTION(fileUrl, "This should always succeed!"); rv = fileUrl->SetQuery(NS_LITERAL_CSTRING("origin=") + aOrigin); NS_ENSURE_SUCCESS(rv, nullptr); return fileUrl.forget(); } // static already_AddRefed IDBFactory::GetConnection(const nsAString& aDatabaseFilePath, const nsACString& aOrigin) { NS_ASSERTION(IndexedDatabaseManager::IsMainProcess(), "Wrong process!"); NS_ASSERTION(StringEndsWith(aDatabaseFilePath, NS_LITERAL_STRING(".sqlite")), "Bad file path!"); nsCOMPtr dbFile(do_CreateInstance(NS_LOCAL_FILE_CONTRACTID)); NS_ENSURE_TRUE(dbFile, nullptr); nsresult rv = dbFile->InitWithPath(aDatabaseFilePath); NS_ENSURE_SUCCESS(rv, nullptr); bool exists; rv = dbFile->Exists(&exists); NS_ENSURE_SUCCESS(rv, nullptr); NS_ENSURE_TRUE(exists, nullptr); nsCOMPtr dbFileUrl = GetDatabaseFileURL(dbFile, aOrigin); NS_ENSURE_TRUE(dbFileUrl, nullptr); nsCOMPtr ss = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); NS_ENSURE_TRUE(ss, nullptr); nsCOMPtr connection; rv = ss->OpenDatabaseWithFileURL(dbFileUrl, getter_AddRefs(connection)); NS_ENSURE_SUCCESS(rv, nullptr); rv = SetDefaultPragmas(connection); NS_ENSURE_SUCCESS(rv, nullptr); return connection.forget(); } // static nsresult IDBFactory::SetDefaultPragmas(mozIStorageConnection* aConnection) { NS_ASSERTION(aConnection, "Null connection!"); static const char query[] = #if defined(MOZ_WIDGET_ANDROID) || defined(MOZ_WIDGET_GONK) // Switch the journaling mode to TRUNCATE to avoid changing the directory // structure at the conclusion of every transaction for devices with slower // file systems. "PRAGMA journal_mode = TRUNCATE; " #endif // We use foreign keys in lots of places. "PRAGMA foreign_keys = ON; " // The "INSERT OR REPLACE" statement doesn't fire the update trigger, // instead it fires only the insert trigger. This confuses the update // refcount function. This behavior changes with enabled recursive triggers, // so the statement fires the delete trigger first and then the insert // trigger. "PRAGMA recursive_triggers = ON;"; nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(query)); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); return NS_OK; } inline bool IgnoreWhitespace(PRUnichar c) { return false; } // static nsresult IDBFactory::LoadDatabaseInformation(mozIStorageConnection* aConnection, nsIAtom* aDatabaseId, uint64_t* aVersion, ObjectStoreInfoArray& aObjectStores) { AssertIsOnIOThread(); NS_ASSERTION(aConnection, "Null pointer!"); aObjectStores.Clear(); // Load object store names and ids. nsCOMPtr stmt; nsresult rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT name, id, key_path, auto_increment " "FROM object_store" ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); nsAutoTArray infoMap; bool hasResult; while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { nsRefPtr* element = aObjectStores.AppendElement(new ObjectStoreInfo()); ObjectStoreInfo* info = element->get(); rv = stmt->GetString(0, info->name); NS_ENSURE_SUCCESS(rv, rv); info->id = stmt->AsInt64(1); int32_t columnType; nsresult rv = stmt->GetTypeOfIndex(2, &columnType); NS_ENSURE_SUCCESS(rv, rv); // NB: We don't have to handle the NULL case, since that is the default // for a new KeyPath. if (columnType != mozIStorageStatement::VALUE_TYPE_NULL) { NS_ASSERTION(columnType == mozIStorageStatement::VALUE_TYPE_TEXT, "Should be a string"); nsString keyPathSerialization; rv = stmt->GetString(2, keyPathSerialization); NS_ENSURE_SUCCESS(rv, rv); info->keyPath = KeyPath::DeserializeFromString(keyPathSerialization); } info->nextAutoIncrementId = stmt->AsInt64(3); info->comittedAutoIncrementId = info->nextAutoIncrementId; info->autoIncrement = !!info->nextAutoIncrementId; ObjectStoreInfoMap* mapEntry = infoMap.AppendElement(); NS_ENSURE_TRUE(mapEntry, NS_ERROR_OUT_OF_MEMORY); mapEntry->id = info->id; mapEntry->info = info; } NS_ENSURE_SUCCESS(rv, rv); // Load index information rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT object_store_id, id, name, key_path, unique_index, multientry " "FROM object_store_index" ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) { int64_t objectStoreId = stmt->AsInt64(0); ObjectStoreInfo* objectStoreInfo = nullptr; for (uint32_t index = 0; index < infoMap.Length(); index++) { if (infoMap[index].id == objectStoreId) { objectStoreInfo = infoMap[index].info; break; } } if (!objectStoreInfo) { NS_ERROR("Index for nonexistant object store!"); return NS_ERROR_UNEXPECTED; } IndexInfo* indexInfo = objectStoreInfo->indexes.AppendElement(); NS_ENSURE_TRUE(indexInfo, NS_ERROR_OUT_OF_MEMORY); indexInfo->id = stmt->AsInt64(1); rv = stmt->GetString(2, indexInfo->name); NS_ENSURE_SUCCESS(rv, rv); nsString keyPathSerialization; rv = stmt->GetString(3, keyPathSerialization); NS_ENSURE_SUCCESS(rv, rv); // XXX bent wants to assert here indexInfo->keyPath = KeyPath::DeserializeFromString(keyPathSerialization); indexInfo->unique = !!stmt->AsInt32(4); indexInfo->multiEntry = !!stmt->AsInt32(5); } NS_ENSURE_SUCCESS(rv, rv); // Load version information. rv = aConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT version " "FROM database" ), getter_AddRefs(stmt)); NS_ENSURE_SUCCESS(rv, rv); rv = stmt->ExecuteStep(&hasResult); NS_ENSURE_SUCCESS(rv, rv); if (!hasResult) { NS_ERROR("Database has no version!"); return NS_ERROR_UNEXPECTED; } int64_t version = 0; rv = stmt->GetInt64(0, &version); *aVersion = std::max(version, 0); return rv; } // static nsresult IDBFactory::SetDatabaseMetadata(DatabaseInfo* aDatabaseInfo, uint64_t aVersion, ObjectStoreInfoArray& aObjectStores) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(aDatabaseInfo, "Null pointer!"); ObjectStoreInfoArray objectStores; objectStores.SwapElements(aObjectStores); #ifdef DEBUG { nsTArray existingNames; aDatabaseInfo->GetObjectStoreNames(existingNames); NS_ASSERTION(existingNames.IsEmpty(), "Should be an empty DatabaseInfo"); } #endif aDatabaseInfo->version = aVersion; for (uint32_t index = 0; index < objectStores.Length(); index++) { nsRefPtr& info = objectStores[index]; if (!aDatabaseInfo->PutObjectStore(info)) { NS_WARNING("Out of memory!"); return NS_ERROR_OUT_OF_MEMORY; } } return NS_OK; } NS_IMPL_CYCLE_COLLECTING_ADDREF(IDBFactory) NS_IMPL_CYCLE_COLLECTING_RELEASE(IDBFactory) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IDBFactory) NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY NS_INTERFACE_MAP_ENTRY(nsISupports) NS_INTERFACE_MAP_END NS_IMPL_CYCLE_COLLECTION_CLASS(IDBFactory) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IDBFactory) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IDBFactory) NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER if (tmp->mOwningObject) { tmp->mOwningObject = nullptr; } if (tmp->mRootedOwningObject) { NS_DROP_JS_OBJECTS(tmp, IDBFactory); tmp->mRootedOwningObject = false; } NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow) NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(IDBFactory) NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mOwningObject) NS_IMPL_CYCLE_COLLECTION_TRACE_END DOMCI_DATA(IDBFactory, IDBFactory) nsresult IDBFactory::OpenInternal(const nsAString& aName, int64_t aVersion, const nsACString& aASCIIOrigin, bool aDeleting, IDBOpenDBRequest** _retval) { NS_ASSERTION(NS_IsMainThread(), "Wrong thread!"); NS_ASSERTION(mWindow || mOwningObject, "Must have one of these!"); AutoJSContext cx; nsCOMPtr window; JS::Rooted scriptOwner(cx); StoragePrivilege privilege; if (mWindow) { window = mWindow; scriptOwner = static_cast(window.get())->FastGetGlobalJSObject(); privilege = Content; } else { scriptOwner = mOwningObject; privilege = Chrome; } nsRefPtr request = IDBOpenDBRequest::Create(this, window, scriptOwner); NS_ENSURE_TRUE(request, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); nsresult rv; if (IndexedDatabaseManager::IsMainProcess()) { nsRefPtr openHelper = new OpenDatabaseHelper(request, aName, aASCIIOrigin, aVersion, aDeleting, mContentParent, privilege); rv = openHelper->Init(); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); nsRefPtr permissionHelper = new CheckPermissionsHelper(openHelper, window, aDeleting); QuotaManager* quotaManager = QuotaManager::Get(); NS_ASSERTION(quotaManager, "This should never be null!"); rv = quotaManager->WaitForOpenAllowed(OriginOrPatternString::FromOrigin( aASCIIOrigin), openHelper->Id(), permissionHelper); NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); } else if (aDeleting) { nsCOMPtr databaseId = QuotaManager::GetStorageId(aASCIIOrigin, aName); NS_ENSURE_TRUE(databaseId, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); IndexedDBDeleteDatabaseRequestChild* actor = new IndexedDBDeleteDatabaseRequestChild(this, request, databaseId); mActorChild->SendPIndexedDBDeleteDatabaseRequestConstructor( actor, nsString(aName)); } else { IndexedDBDatabaseChild* dbActor = static_cast( mActorChild->SendPIndexedDBDatabaseConstructor(nsString(aName), aVersion)); dbActor->SetRequest(request); } #ifdef IDB_PROFILER_USE_MARKS { NS_ConvertUTF16toUTF8 profilerName(aName); if (aDeleting) { IDB_PROFILER_MARK("IndexedDB Request %llu: deleteDatabase(\"%s\")", "MT IDBFactory.deleteDatabase()", request->GetSerialNumber(), profilerName.get()); } else { IDB_PROFILER_MARK("IndexedDB Request %llu: open(\"%s\", %lld)", "MT IDBFactory.open()", request->GetSerialNumber(), profilerName.get(), aVersion); } } #endif request.forget(_retval); return NS_OK; } JSObject* IDBFactory::WrapObject(JSContext* aCx, JS::Handle aScope) { return IDBFactoryBinding::Wrap(aCx, aScope, this); } int16_t IDBFactory::Cmp(JSContext* aCx, JS::Handle aFirst, JS::Handle aSecond, ErrorResult& aRv) { Key first, second; nsresult rv = first.SetFromJSVal(aCx, aFirst); if (NS_FAILED(rv)) { aRv.Throw(rv); return 0; } rv = second.SetFromJSVal(aCx, aSecond); if (NS_FAILED(rv)) { aRv.Throw(rv); return 0; } if (first.IsUnset() || second.IsUnset()) { aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR); return 0; } return Key::CompareKeys(first, second); } already_AddRefed IDBFactory::OpenForPrincipal(nsIPrincipal* aPrincipal, const nsAString& aName, const Optional& aVersion, ErrorResult& aRv) { // Just to be on the extra-safe side if (!nsContentUtils::IsCallerChrome()) { MOZ_CRASH(); } return Open(aPrincipal, aName, aVersion, false, aRv); } already_AddRefed IDBFactory::DeleteForPrincipal(nsIPrincipal* aPrincipal, const nsAString& aName, ErrorResult& aRv) { // Just to be on the extra-safe side if (!nsContentUtils::IsCallerChrome()) { MOZ_CRASH(); } return Open(aPrincipal, aName, Optional(), true, aRv); } already_AddRefed IDBFactory::Open(nsIPrincipal* aPrincipal, const nsAString& aName, const Optional& aVersion, bool aDelete, ErrorResult& aRv) { nsresult rv; nsCString origin; if (aPrincipal) { rv = QuotaManager::GetASCIIOriginFromPrincipal(aPrincipal, origin); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } } else { origin = mASCIIOrigin; } uint64_t version; if (!aDelete && aVersion.WasPassed()) { version = aVersion.Value(); if (version < 1) { aRv.ThrowTypeError(MSG_INVALID_VERSION); return nullptr; } } else { version = 0; } nsRefPtr request; rv = OpenInternal(aName, version, origin, aDelete, getter_AddRefs(request)); if (NS_FAILED(rv)) { aRv.Throw(rv); return nullptr; } return request.forget(); }