michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* vim: set ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "KeyPath.h" michael@0: #include "IDBObjectStore.h" michael@0: #include "Key.h" michael@0: #include "ReportInternalError.h" michael@0: michael@0: #include "nsCharSeparatedTokenizer.h" michael@0: #include "nsJSUtils.h" michael@0: #include "xpcpublic.h" michael@0: michael@0: #include "mozilla/dom/BindingDeclarations.h" michael@0: michael@0: USING_INDEXEDDB_NAMESPACE michael@0: michael@0: namespace { michael@0: michael@0: inline michael@0: bool michael@0: IgnoreWhitespace(char16_t c) michael@0: { michael@0: return false; michael@0: } michael@0: michael@0: typedef nsCharSeparatedTokenizerTemplate KeyPathTokenizer; michael@0: michael@0: bool michael@0: IsValidKeyPathString(JSContext* aCx, const nsAString& aKeyPath) michael@0: { michael@0: NS_ASSERTION(!aKeyPath.IsVoid(), "What?"); michael@0: michael@0: KeyPathTokenizer tokenizer(aKeyPath, '.'); michael@0: michael@0: while (tokenizer.hasMoreTokens()) { michael@0: nsString token(tokenizer.nextToken()); michael@0: michael@0: if (!token.Length()) { michael@0: return false; michael@0: } michael@0: michael@0: JS::Rooted stringVal(aCx); michael@0: if (!xpc::StringToJsval(aCx, token, &stringVal)) { michael@0: return false; michael@0: } michael@0: michael@0: NS_ASSERTION(stringVal.toString(), "This should never happen"); michael@0: JS::Rooted str(aCx, stringVal.toString()); michael@0: michael@0: bool isIdentifier = false; michael@0: if (!JS_IsIdentifier(aCx, str, &isIdentifier) || !isIdentifier) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: // If the very last character was a '.', the tokenizer won't give us an empty michael@0: // token, but the keyPath is still invalid. michael@0: if (!aKeyPath.IsEmpty() && michael@0: aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: enum KeyExtractionOptions { michael@0: DoNotCreateProperties, michael@0: CreateProperties michael@0: }; michael@0: michael@0: nsresult michael@0: GetJSValFromKeyPathString(JSContext* aCx, michael@0: const JS::Value& aValue, michael@0: const nsAString& aKeyPathString, michael@0: JS::Value* aKeyJSVal, michael@0: KeyExtractionOptions aOptions, michael@0: KeyPath::ExtractOrCreateKeyCallback aCallback, michael@0: void* aClosure) michael@0: { michael@0: NS_ASSERTION(aCx, "Null pointer!"); michael@0: NS_ASSERTION(IsValidKeyPathString(aCx, aKeyPathString), michael@0: "This will explode!"); michael@0: NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties, michael@0: "This is not allowed!"); michael@0: NS_ASSERTION(aOptions != CreateProperties || aCallback, michael@0: "If properties are created, there must be a callback!"); michael@0: michael@0: nsresult rv = NS_OK; michael@0: *aKeyJSVal = aValue; michael@0: michael@0: KeyPathTokenizer tokenizer(aKeyPathString, '.'); michael@0: michael@0: nsString targetObjectPropName; michael@0: JS::Rooted targetObject(aCx, nullptr); michael@0: JS::Rooted obj(aCx, michael@0: JSVAL_IS_PRIMITIVE(aValue) ? nullptr : JSVAL_TO_OBJECT(aValue)); michael@0: michael@0: while (tokenizer.hasMoreTokens()) { michael@0: const nsDependentSubstring& token = tokenizer.nextToken(); michael@0: michael@0: NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath"); michael@0: michael@0: const jschar* keyPathChars = token.BeginReading(); michael@0: const size_t keyPathLen = token.Length(); michael@0: michael@0: bool hasProp; michael@0: if (!targetObject) { michael@0: // We're still walking the chain of existing objects michael@0: if (!obj) { michael@0: return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: michael@0: bool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen, michael@0: &hasProp); michael@0: IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); michael@0: michael@0: if (hasProp) { michael@0: // Get if the property exists... michael@0: JS::Rooted intermediate(aCx); michael@0: bool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen, &intermediate); michael@0: IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); michael@0: michael@0: // Treat explicitly undefined as an error. michael@0: if (intermediate == JSVAL_VOID) { michael@0: return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: if (tokenizer.hasMoreTokens()) { michael@0: // ...and walk to it if there are more steps... michael@0: if (JSVAL_IS_PRIMITIVE(intermediate)) { michael@0: return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: obj = JSVAL_TO_OBJECT(intermediate); michael@0: } michael@0: else { michael@0: // ...otherwise use it as key michael@0: *aKeyJSVal = intermediate; michael@0: } michael@0: } michael@0: else { michael@0: // If the property doesn't exist, fall into below path of starting michael@0: // to define properties, if allowed. michael@0: if (aOptions == DoNotCreateProperties) { michael@0: return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: michael@0: targetObject = obj; michael@0: targetObjectPropName = token; michael@0: } michael@0: } michael@0: michael@0: if (targetObject) { michael@0: // We have started inserting new objects or are about to just insert michael@0: // the first one. michael@0: michael@0: *aKeyJSVal = JSVAL_VOID; michael@0: michael@0: if (tokenizer.hasMoreTokens()) { michael@0: // If we're not at the end, we need to add a dummy object to the michael@0: // chain. michael@0: JS::Rooted dummy(aCx, JS_NewObject(aCx, nullptr, JS::NullPtr(), michael@0: JS::NullPtr())); michael@0: if (!dummy) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: break; michael@0: } michael@0: michael@0: if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), michael@0: token.Length(), michael@0: OBJECT_TO_JSVAL(dummy), nullptr, nullptr, michael@0: JSPROP_ENUMERATE)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: break; michael@0: } michael@0: michael@0: obj = dummy; michael@0: } michael@0: else { michael@0: JS::Rooted dummy(aCx, JS_NewObject(aCx, &IDBObjectStore::sDummyPropJSClass, michael@0: JS::NullPtr(), JS::NullPtr())); michael@0: if (!dummy) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: break; michael@0: } michael@0: michael@0: if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), michael@0: token.Length(), OBJECT_TO_JSVAL(dummy), michael@0: nullptr, nullptr, JSPROP_ENUMERATE)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: break; michael@0: } michael@0: michael@0: obj = dummy; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // We guard on rv being a success because we need to run the property michael@0: // deletion code below even if we should not be running the callback. michael@0: if (NS_SUCCEEDED(rv) && aCallback) { michael@0: rv = (*aCallback)(aCx, aClosure); michael@0: } michael@0: michael@0: if (targetObject) { michael@0: // If this fails, we lose, and the web page sees a magical property michael@0: // appear on the object :-( michael@0: bool succeeded; michael@0: if (!JS_DeleteUCProperty2(aCx, targetObject, michael@0: targetObjectPropName.get(), michael@0: targetObjectPropName.Length(), michael@0: &succeeded)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); michael@0: } michael@0: michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: return rv; michael@0: } michael@0: michael@0: } // anonymous namespace michael@0: michael@0: // static michael@0: nsresult michael@0: KeyPath::Parse(JSContext* aCx, const nsAString& aString, KeyPath* aKeyPath) michael@0: { michael@0: KeyPath keyPath(0); michael@0: keyPath.SetType(STRING); michael@0: michael@0: if (!keyPath.AppendStringWithValidation(aCx, aString)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: *aKeyPath = keyPath; michael@0: return NS_OK; michael@0: } michael@0: michael@0: //static michael@0: nsresult michael@0: KeyPath::Parse(JSContext* aCx, const mozilla::dom::Sequence& aStrings, michael@0: KeyPath* aKeyPath) michael@0: { michael@0: KeyPath keyPath(0); michael@0: keyPath.SetType(ARRAY); michael@0: michael@0: for (uint32_t i = 0; i < aStrings.Length(); ++i) { michael@0: if (!keyPath.AppendStringWithValidation(aCx, aStrings[i])) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: michael@0: *aKeyPath = keyPath; michael@0: return NS_OK; michael@0: } michael@0: michael@0: // static michael@0: nsresult michael@0: KeyPath::Parse(JSContext* aCx, const JS::Value& aValue_, KeyPath* aKeyPath) michael@0: { michael@0: JS::Rooted aValue(aCx, aValue_); michael@0: KeyPath keyPath(0); michael@0: michael@0: aKeyPath->SetType(NONEXISTENT); michael@0: michael@0: // See if this is a JS array. michael@0: if (JS_IsArrayObject(aCx, aValue)) { michael@0: michael@0: JS::Rooted obj(aCx, JSVAL_TO_OBJECT(aValue)); michael@0: michael@0: uint32_t length; michael@0: if (!JS_GetArrayLength(aCx, obj, &length)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: if (!length) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: keyPath.SetType(ARRAY); michael@0: michael@0: for (uint32_t index = 0; index < length; index++) { michael@0: JS::Rooted val(aCx); michael@0: JSString* jsstr; michael@0: nsDependentJSString str; michael@0: if (!JS_GetElement(aCx, obj, index, &val) || michael@0: !(jsstr = JS::ToString(aCx, val)) || michael@0: !str.init(aCx, jsstr)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: if (!keyPath.AppendStringWithValidation(aCx, str)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: } michael@0: // Otherwise convert it to a string. michael@0: else if (!JSVAL_IS_NULL(aValue) && !JSVAL_IS_VOID(aValue)) { michael@0: JSString* jsstr; michael@0: nsDependentJSString str; michael@0: if (!(jsstr = JS::ToString(aCx, aValue)) || michael@0: !str.init(aCx, jsstr)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: keyPath.SetType(STRING); michael@0: michael@0: if (!keyPath.AppendStringWithValidation(aCx, str)) { michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: } michael@0: michael@0: *aKeyPath = keyPath; michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: KeyPath::SetType(KeyPathType aType) michael@0: { michael@0: mType = aType; michael@0: mStrings.Clear(); michael@0: } michael@0: michael@0: bool michael@0: KeyPath::AppendStringWithValidation(JSContext* aCx, const nsAString& aString) michael@0: { michael@0: if (!IsValidKeyPathString(aCx, aString)) { michael@0: return false; michael@0: } michael@0: michael@0: if (IsString()) { michael@0: NS_ASSERTION(mStrings.Length() == 0, "Too many strings!"); michael@0: mStrings.AppendElement(aString); michael@0: return true; michael@0: } michael@0: michael@0: if (IsArray()) { michael@0: mStrings.AppendElement(aString); michael@0: return true; michael@0: } michael@0: michael@0: NS_NOTREACHED("What?!"); michael@0: return false; michael@0: } michael@0: michael@0: nsresult michael@0: KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const michael@0: { michael@0: uint32_t len = mStrings.Length(); michael@0: JS::Rooted value(aCx); michael@0: michael@0: aKey.Unset(); michael@0: michael@0: for (uint32_t i = 0; i < len; ++i) { michael@0: nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], michael@0: value.address(), michael@0: DoNotCreateProperties, nullptr, michael@0: nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) { michael@0: NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset"); michael@0: return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: } michael@0: michael@0: aKey.FinishArray(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, michael@0: JS::Value* aOutVal) const michael@0: { michael@0: NS_ASSERTION(IsValid(), "This doesn't make sense!"); michael@0: michael@0: if (IsString()) { michael@0: return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal, michael@0: DoNotCreateProperties, nullptr, nullptr); michael@0: } michael@0: michael@0: const uint32_t len = mStrings.Length(); michael@0: JS::Rooted arrayObj(aCx, JS_NewArrayObject(aCx, len)); michael@0: if (!arrayObj) { michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: michael@0: JS::Rooted value(aCx); michael@0: for (uint32_t i = 0; i < len; ++i) { michael@0: nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], michael@0: value.address(), michael@0: DoNotCreateProperties, nullptr, michael@0: nullptr); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (!JS_SetElement(aCx, arrayObj, i, value)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: } michael@0: michael@0: *aOutVal = OBJECT_TO_JSVAL(arrayObj); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, michael@0: Key& aKey, ExtractOrCreateKeyCallback aCallback, michael@0: void* aClosure) const michael@0: { michael@0: NS_ASSERTION(IsString(), "This doesn't make sense!"); michael@0: michael@0: JS::Rooted value(aCx); michael@0: michael@0: aKey.Unset(); michael@0: michael@0: nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[0], michael@0: value.address(), michael@0: CreateProperties, aCallback, michael@0: aClosure); michael@0: if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: if (NS_FAILED(aKey.AppendItem(aCx, false, value))) { michael@0: NS_ASSERTION(aKey.IsUnset(), "Should be unset"); michael@0: return JSVAL_IS_VOID(value) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR; michael@0: } michael@0: michael@0: aKey.FinishArray(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: KeyPath::SerializeToString(nsAString& aString) const michael@0: { michael@0: NS_ASSERTION(IsValid(), "Check to see if I'm valid first!"); michael@0: michael@0: if (IsString()) { michael@0: aString = mStrings[0]; michael@0: return; michael@0: } michael@0: michael@0: if (IsArray()) { michael@0: // We use a comma in the beginning to indicate that it's an array of michael@0: // key paths. This is to be able to tell a string-keypath from an michael@0: // array-keypath which contains only one item. michael@0: // It also makes serializing easier :-) michael@0: uint32_t len = mStrings.Length(); michael@0: for (uint32_t i = 0; i < len; ++i) { michael@0: aString.Append(NS_LITERAL_STRING(",") + mStrings[i]); michael@0: } michael@0: michael@0: return; michael@0: } michael@0: michael@0: NS_NOTREACHED("What?"); michael@0: } michael@0: michael@0: // static michael@0: KeyPath michael@0: KeyPath::DeserializeFromString(const nsAString& aString) michael@0: { michael@0: KeyPath keyPath(0); michael@0: michael@0: if (!aString.IsEmpty() && aString.First() == ',') { michael@0: keyPath.SetType(ARRAY); michael@0: michael@0: // We use a comma in the beginning to indicate that it's an array of michael@0: // key paths. This is to be able to tell a string-keypath from an michael@0: // array-keypath which contains only one item. michael@0: nsCharSeparatedTokenizerTemplate tokenizer(aString, ','); michael@0: tokenizer.nextToken(); michael@0: while (tokenizer.hasMoreTokens()) { michael@0: keyPath.mStrings.AppendElement(tokenizer.nextToken()); michael@0: } michael@0: michael@0: return keyPath; michael@0: } michael@0: michael@0: keyPath.SetType(STRING); michael@0: keyPath.mStrings.AppendElement(aString); michael@0: michael@0: return keyPath; michael@0: } michael@0: michael@0: nsresult michael@0: KeyPath::ToJSVal(JSContext* aCx, JS::MutableHandle aValue) const michael@0: { michael@0: if (IsArray()) { michael@0: uint32_t len = mStrings.Length(); michael@0: JS::Rooted array(aCx, JS_NewArrayObject(aCx, len)); michael@0: if (!array) { michael@0: IDB_WARNING("Failed to make array!"); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: michael@0: for (uint32_t i = 0; i < len; ++i) { michael@0: JS::Rooted val(aCx); michael@0: nsString tmp(mStrings[i]); michael@0: if (!xpc::StringToJsval(aCx, tmp, &val)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: michael@0: if (!JS_SetElement(aCx, array, i, val)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: } michael@0: michael@0: aValue.setObject(*array); michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (IsString()) { michael@0: nsString tmp(mStrings[0]); michael@0: if (!xpc::StringToJsval(aCx, tmp, aValue)) { michael@0: IDB_REPORT_INTERNAL_ERR(); michael@0: return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: aValue.setNull(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: KeyPath::ToJSVal(JSContext* aCx, JS::Heap& aValue) const michael@0: { michael@0: JS::Rooted value(aCx); michael@0: nsresult rv = ToJSVal(aCx, &value); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: aValue = value; michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: bool michael@0: KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const michael@0: { michael@0: // Any keypath that passed validation is allowed for non-autoIncrement michael@0: // objectStores. michael@0: if (!aAutoIncrement) { michael@0: return true; michael@0: } michael@0: michael@0: // Array keypaths are not allowed for autoIncrement objectStores. michael@0: if (IsArray()) { michael@0: return false; michael@0: } michael@0: michael@0: // Neither are empty strings. michael@0: if (IsEmpty()) { michael@0: return false; michael@0: } michael@0: michael@0: // Everything else is ok. michael@0: return true; michael@0: }