1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/indexedDB/KeyPath.cpp Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,573 @@ 1.4 +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +#include "KeyPath.h" 1.11 +#include "IDBObjectStore.h" 1.12 +#include "Key.h" 1.13 +#include "ReportInternalError.h" 1.14 + 1.15 +#include "nsCharSeparatedTokenizer.h" 1.16 +#include "nsJSUtils.h" 1.17 +#include "xpcpublic.h" 1.18 + 1.19 +#include "mozilla/dom/BindingDeclarations.h" 1.20 + 1.21 +USING_INDEXEDDB_NAMESPACE 1.22 + 1.23 +namespace { 1.24 + 1.25 +inline 1.26 +bool 1.27 +IgnoreWhitespace(char16_t c) 1.28 +{ 1.29 + return false; 1.30 +} 1.31 + 1.32 +typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer; 1.33 + 1.34 +bool 1.35 +IsValidKeyPathString(JSContext* aCx, const nsAString& aKeyPath) 1.36 +{ 1.37 + NS_ASSERTION(!aKeyPath.IsVoid(), "What?"); 1.38 + 1.39 + KeyPathTokenizer tokenizer(aKeyPath, '.'); 1.40 + 1.41 + while (tokenizer.hasMoreTokens()) { 1.42 + nsString token(tokenizer.nextToken()); 1.43 + 1.44 + if (!token.Length()) { 1.45 + return false; 1.46 + } 1.47 + 1.48 + JS::Rooted<JS::Value> stringVal(aCx); 1.49 + if (!xpc::StringToJsval(aCx, token, &stringVal)) { 1.50 + return false; 1.51 + } 1.52 + 1.53 + NS_ASSERTION(stringVal.toString(), "This should never happen"); 1.54 + JS::Rooted<JSString*> str(aCx, stringVal.toString()); 1.55 + 1.56 + bool isIdentifier = false; 1.57 + if (!JS_IsIdentifier(aCx, str, &isIdentifier) || !isIdentifier) { 1.58 + return false; 1.59 + } 1.60 + } 1.61 + 1.62 + // If the very last character was a '.', the tokenizer won't give us an empty 1.63 + // token, but the keyPath is still invalid. 1.64 + if (!aKeyPath.IsEmpty() && 1.65 + aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') { 1.66 + return false; 1.67 + } 1.68 + 1.69 + return true; 1.70 +} 1.71 + 1.72 +enum KeyExtractionOptions { 1.73 + DoNotCreateProperties, 1.74 + CreateProperties 1.75 +}; 1.76 + 1.77 +nsresult 1.78 +GetJSValFromKeyPathString(JSContext* aCx, 1.79 + const JS::Value& aValue, 1.80 + const nsAString& aKeyPathString, 1.81 + JS::Value* aKeyJSVal, 1.82 + KeyExtractionOptions aOptions, 1.83 + KeyPath::ExtractOrCreateKeyCallback aCallback, 1.84 + void* aClosure) 1.85 +{ 1.86 + NS_ASSERTION(aCx, "Null pointer!"); 1.87 + NS_ASSERTION(IsValidKeyPathString(aCx, aKeyPathString), 1.88 + "This will explode!"); 1.89 + NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties, 1.90 + "This is not allowed!"); 1.91 + NS_ASSERTION(aOptions != CreateProperties || aCallback, 1.92 + "If properties are created, there must be a callback!"); 1.93 + 1.94 + nsresult rv = NS_OK; 1.95 + *aKeyJSVal = aValue; 1.96 + 1.97 + KeyPathTokenizer tokenizer(aKeyPathString, '.'); 1.98 + 1.99 + nsString targetObjectPropName; 1.100 + JS::Rooted<JSObject*> targetObject(aCx, nullptr); 1.101 + JS::Rooted<JSObject*> obj(aCx, 1.102 + JSVAL_IS_PRIMITIVE(aValue) ? nullptr : JSVAL_TO_OBJECT(aValue)); 1.103 + 1.104 + while (tokenizer.hasMoreTokens()) { 1.105 + const nsDependentSubstring& token = tokenizer.nextToken(); 1.106 + 1.107 + NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath"); 1.108 + 1.109 + const jschar* keyPathChars = token.BeginReading(); 1.110 + const size_t keyPathLen = token.Length(); 1.111 + 1.112 + bool hasProp; 1.113 + if (!targetObject) { 1.114 + // We're still walking the chain of existing objects 1.115 + if (!obj) { 1.116 + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.117 + } 1.118 + 1.119 + bool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen, 1.120 + &hasProp); 1.121 + IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); 1.122 + 1.123 + if (hasProp) { 1.124 + // Get if the property exists... 1.125 + JS::Rooted<JS::Value> intermediate(aCx); 1.126 + bool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen, &intermediate); 1.127 + IDB_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); 1.128 + 1.129 + // Treat explicitly undefined as an error. 1.130 + if (intermediate == JSVAL_VOID) { 1.131 + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.132 + } 1.133 + if (tokenizer.hasMoreTokens()) { 1.134 + // ...and walk to it if there are more steps... 1.135 + if (JSVAL_IS_PRIMITIVE(intermediate)) { 1.136 + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.137 + } 1.138 + obj = JSVAL_TO_OBJECT(intermediate); 1.139 + } 1.140 + else { 1.141 + // ...otherwise use it as key 1.142 + *aKeyJSVal = intermediate; 1.143 + } 1.144 + } 1.145 + else { 1.146 + // If the property doesn't exist, fall into below path of starting 1.147 + // to define properties, if allowed. 1.148 + if (aOptions == DoNotCreateProperties) { 1.149 + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.150 + } 1.151 + 1.152 + targetObject = obj; 1.153 + targetObjectPropName = token; 1.154 + } 1.155 + } 1.156 + 1.157 + if (targetObject) { 1.158 + // We have started inserting new objects or are about to just insert 1.159 + // the first one. 1.160 + 1.161 + *aKeyJSVal = JSVAL_VOID; 1.162 + 1.163 + if (tokenizer.hasMoreTokens()) { 1.164 + // If we're not at the end, we need to add a dummy object to the 1.165 + // chain. 1.166 + JS::Rooted<JSObject*> dummy(aCx, JS_NewObject(aCx, nullptr, JS::NullPtr(), 1.167 + JS::NullPtr())); 1.168 + if (!dummy) { 1.169 + IDB_REPORT_INTERNAL_ERR(); 1.170 + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.171 + break; 1.172 + } 1.173 + 1.174 + if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), 1.175 + token.Length(), 1.176 + OBJECT_TO_JSVAL(dummy), nullptr, nullptr, 1.177 + JSPROP_ENUMERATE)) { 1.178 + IDB_REPORT_INTERNAL_ERR(); 1.179 + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.180 + break; 1.181 + } 1.182 + 1.183 + obj = dummy; 1.184 + } 1.185 + else { 1.186 + JS::Rooted<JSObject*> dummy(aCx, JS_NewObject(aCx, &IDBObjectStore::sDummyPropJSClass, 1.187 + JS::NullPtr(), JS::NullPtr())); 1.188 + if (!dummy) { 1.189 + IDB_REPORT_INTERNAL_ERR(); 1.190 + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.191 + break; 1.192 + } 1.193 + 1.194 + if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), 1.195 + token.Length(), OBJECT_TO_JSVAL(dummy), 1.196 + nullptr, nullptr, JSPROP_ENUMERATE)) { 1.197 + IDB_REPORT_INTERNAL_ERR(); 1.198 + rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.199 + break; 1.200 + } 1.201 + 1.202 + obj = dummy; 1.203 + } 1.204 + } 1.205 + } 1.206 + 1.207 + // We guard on rv being a success because we need to run the property 1.208 + // deletion code below even if we should not be running the callback. 1.209 + if (NS_SUCCEEDED(rv) && aCallback) { 1.210 + rv = (*aCallback)(aCx, aClosure); 1.211 + } 1.212 + 1.213 + if (targetObject) { 1.214 + // If this fails, we lose, and the web page sees a magical property 1.215 + // appear on the object :-( 1.216 + bool succeeded; 1.217 + if (!JS_DeleteUCProperty2(aCx, targetObject, 1.218 + targetObjectPropName.get(), 1.219 + targetObjectPropName.Length(), 1.220 + &succeeded)) { 1.221 + IDB_REPORT_INTERNAL_ERR(); 1.222 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.223 + } 1.224 + IDB_ENSURE_TRUE(succeeded, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR); 1.225 + } 1.226 + 1.227 + NS_ENSURE_SUCCESS(rv, rv); 1.228 + return rv; 1.229 +} 1.230 + 1.231 +} // anonymous namespace 1.232 + 1.233 +// static 1.234 +nsresult 1.235 +KeyPath::Parse(JSContext* aCx, const nsAString& aString, KeyPath* aKeyPath) 1.236 +{ 1.237 + KeyPath keyPath(0); 1.238 + keyPath.SetType(STRING); 1.239 + 1.240 + if (!keyPath.AppendStringWithValidation(aCx, aString)) { 1.241 + return NS_ERROR_FAILURE; 1.242 + } 1.243 + 1.244 + *aKeyPath = keyPath; 1.245 + return NS_OK; 1.246 +} 1.247 + 1.248 +//static 1.249 +nsresult 1.250 +KeyPath::Parse(JSContext* aCx, const mozilla::dom::Sequence<nsString>& aStrings, 1.251 + KeyPath* aKeyPath) 1.252 +{ 1.253 + KeyPath keyPath(0); 1.254 + keyPath.SetType(ARRAY); 1.255 + 1.256 + for (uint32_t i = 0; i < aStrings.Length(); ++i) { 1.257 + if (!keyPath.AppendStringWithValidation(aCx, aStrings[i])) { 1.258 + return NS_ERROR_FAILURE; 1.259 + } 1.260 + } 1.261 + 1.262 + *aKeyPath = keyPath; 1.263 + return NS_OK; 1.264 +} 1.265 + 1.266 +// static 1.267 +nsresult 1.268 +KeyPath::Parse(JSContext* aCx, const JS::Value& aValue_, KeyPath* aKeyPath) 1.269 +{ 1.270 + JS::Rooted<JS::Value> aValue(aCx, aValue_); 1.271 + KeyPath keyPath(0); 1.272 + 1.273 + aKeyPath->SetType(NONEXISTENT); 1.274 + 1.275 + // See if this is a JS array. 1.276 + if (JS_IsArrayObject(aCx, aValue)) { 1.277 + 1.278 + JS::Rooted<JSObject*> obj(aCx, JSVAL_TO_OBJECT(aValue)); 1.279 + 1.280 + uint32_t length; 1.281 + if (!JS_GetArrayLength(aCx, obj, &length)) { 1.282 + return NS_ERROR_FAILURE; 1.283 + } 1.284 + 1.285 + if (!length) { 1.286 + return NS_ERROR_FAILURE; 1.287 + } 1.288 + 1.289 + keyPath.SetType(ARRAY); 1.290 + 1.291 + for (uint32_t index = 0; index < length; index++) { 1.292 + JS::Rooted<JS::Value> val(aCx); 1.293 + JSString* jsstr; 1.294 + nsDependentJSString str; 1.295 + if (!JS_GetElement(aCx, obj, index, &val) || 1.296 + !(jsstr = JS::ToString(aCx, val)) || 1.297 + !str.init(aCx, jsstr)) { 1.298 + return NS_ERROR_FAILURE; 1.299 + } 1.300 + 1.301 + if (!keyPath.AppendStringWithValidation(aCx, str)) { 1.302 + return NS_ERROR_FAILURE; 1.303 + } 1.304 + } 1.305 + } 1.306 + // Otherwise convert it to a string. 1.307 + else if (!JSVAL_IS_NULL(aValue) && !JSVAL_IS_VOID(aValue)) { 1.308 + JSString* jsstr; 1.309 + nsDependentJSString str; 1.310 + if (!(jsstr = JS::ToString(aCx, aValue)) || 1.311 + !str.init(aCx, jsstr)) { 1.312 + return NS_ERROR_FAILURE; 1.313 + } 1.314 + 1.315 + keyPath.SetType(STRING); 1.316 + 1.317 + if (!keyPath.AppendStringWithValidation(aCx, str)) { 1.318 + return NS_ERROR_FAILURE; 1.319 + } 1.320 + } 1.321 + 1.322 + *aKeyPath = keyPath; 1.323 + return NS_OK; 1.324 +} 1.325 + 1.326 +void 1.327 +KeyPath::SetType(KeyPathType aType) 1.328 +{ 1.329 + mType = aType; 1.330 + mStrings.Clear(); 1.331 +} 1.332 + 1.333 +bool 1.334 +KeyPath::AppendStringWithValidation(JSContext* aCx, const nsAString& aString) 1.335 +{ 1.336 + if (!IsValidKeyPathString(aCx, aString)) { 1.337 + return false; 1.338 + } 1.339 + 1.340 + if (IsString()) { 1.341 + NS_ASSERTION(mStrings.Length() == 0, "Too many strings!"); 1.342 + mStrings.AppendElement(aString); 1.343 + return true; 1.344 + } 1.345 + 1.346 + if (IsArray()) { 1.347 + mStrings.AppendElement(aString); 1.348 + return true; 1.349 + } 1.350 + 1.351 + NS_NOTREACHED("What?!"); 1.352 + return false; 1.353 +} 1.354 + 1.355 +nsresult 1.356 +KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const 1.357 +{ 1.358 + uint32_t len = mStrings.Length(); 1.359 + JS::Rooted<JS::Value> value(aCx); 1.360 + 1.361 + aKey.Unset(); 1.362 + 1.363 + for (uint32_t i = 0; i < len; ++i) { 1.364 + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], 1.365 + value.address(), 1.366 + DoNotCreateProperties, nullptr, 1.367 + nullptr); 1.368 + if (NS_FAILED(rv)) { 1.369 + return rv; 1.370 + } 1.371 + 1.372 + if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) { 1.373 + NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset"); 1.374 + return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.375 + } 1.376 + } 1.377 + 1.378 + aKey.FinishArray(); 1.379 + 1.380 + return NS_OK; 1.381 +} 1.382 + 1.383 +nsresult 1.384 +KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, 1.385 + JS::Value* aOutVal) const 1.386 +{ 1.387 + NS_ASSERTION(IsValid(), "This doesn't make sense!"); 1.388 + 1.389 + if (IsString()) { 1.390 + return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal, 1.391 + DoNotCreateProperties, nullptr, nullptr); 1.392 + } 1.393 + 1.394 + const uint32_t len = mStrings.Length(); 1.395 + JS::Rooted<JSObject*> arrayObj(aCx, JS_NewArrayObject(aCx, len)); 1.396 + if (!arrayObj) { 1.397 + return NS_ERROR_OUT_OF_MEMORY; 1.398 + } 1.399 + 1.400 + JS::Rooted<JS::Value> value(aCx); 1.401 + for (uint32_t i = 0; i < len; ++i) { 1.402 + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i], 1.403 + value.address(), 1.404 + DoNotCreateProperties, nullptr, 1.405 + nullptr); 1.406 + if (NS_FAILED(rv)) { 1.407 + return rv; 1.408 + } 1.409 + 1.410 + if (!JS_SetElement(aCx, arrayObj, i, value)) { 1.411 + IDB_REPORT_INTERNAL_ERR(); 1.412 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.413 + } 1.414 + } 1.415 + 1.416 + *aOutVal = OBJECT_TO_JSVAL(arrayObj); 1.417 + return NS_OK; 1.418 +} 1.419 + 1.420 +nsresult 1.421 +KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, 1.422 + Key& aKey, ExtractOrCreateKeyCallback aCallback, 1.423 + void* aClosure) const 1.424 +{ 1.425 + NS_ASSERTION(IsString(), "This doesn't make sense!"); 1.426 + 1.427 + JS::Rooted<JS::Value> value(aCx); 1.428 + 1.429 + aKey.Unset(); 1.430 + 1.431 + nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[0], 1.432 + value.address(), 1.433 + CreateProperties, aCallback, 1.434 + aClosure); 1.435 + if (NS_FAILED(rv)) { 1.436 + return rv; 1.437 + } 1.438 + 1.439 + if (NS_FAILED(aKey.AppendItem(aCx, false, value))) { 1.440 + NS_ASSERTION(aKey.IsUnset(), "Should be unset"); 1.441 + return JSVAL_IS_VOID(value) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 1.442 + } 1.443 + 1.444 + aKey.FinishArray(); 1.445 + 1.446 + return NS_OK; 1.447 +} 1.448 + 1.449 +void 1.450 +KeyPath::SerializeToString(nsAString& aString) const 1.451 +{ 1.452 + NS_ASSERTION(IsValid(), "Check to see if I'm valid first!"); 1.453 + 1.454 + if (IsString()) { 1.455 + aString = mStrings[0]; 1.456 + return; 1.457 + } 1.458 + 1.459 + if (IsArray()) { 1.460 + // We use a comma in the beginning to indicate that it's an array of 1.461 + // key paths. This is to be able to tell a string-keypath from an 1.462 + // array-keypath which contains only one item. 1.463 + // It also makes serializing easier :-) 1.464 + uint32_t len = mStrings.Length(); 1.465 + for (uint32_t i = 0; i < len; ++i) { 1.466 + aString.Append(NS_LITERAL_STRING(",") + mStrings[i]); 1.467 + } 1.468 + 1.469 + return; 1.470 + } 1.471 + 1.472 + NS_NOTREACHED("What?"); 1.473 +} 1.474 + 1.475 +// static 1.476 +KeyPath 1.477 +KeyPath::DeserializeFromString(const nsAString& aString) 1.478 +{ 1.479 + KeyPath keyPath(0); 1.480 + 1.481 + if (!aString.IsEmpty() && aString.First() == ',') { 1.482 + keyPath.SetType(ARRAY); 1.483 + 1.484 + // We use a comma in the beginning to indicate that it's an array of 1.485 + // key paths. This is to be able to tell a string-keypath from an 1.486 + // array-keypath which contains only one item. 1.487 + nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> tokenizer(aString, ','); 1.488 + tokenizer.nextToken(); 1.489 + while (tokenizer.hasMoreTokens()) { 1.490 + keyPath.mStrings.AppendElement(tokenizer.nextToken()); 1.491 + } 1.492 + 1.493 + return keyPath; 1.494 + } 1.495 + 1.496 + keyPath.SetType(STRING); 1.497 + keyPath.mStrings.AppendElement(aString); 1.498 + 1.499 + return keyPath; 1.500 +} 1.501 + 1.502 +nsresult 1.503 +KeyPath::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aValue) const 1.504 +{ 1.505 + if (IsArray()) { 1.506 + uint32_t len = mStrings.Length(); 1.507 + JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, len)); 1.508 + if (!array) { 1.509 + IDB_WARNING("Failed to make array!"); 1.510 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.511 + } 1.512 + 1.513 + for (uint32_t i = 0; i < len; ++i) { 1.514 + JS::Rooted<JS::Value> val(aCx); 1.515 + nsString tmp(mStrings[i]); 1.516 + if (!xpc::StringToJsval(aCx, tmp, &val)) { 1.517 + IDB_REPORT_INTERNAL_ERR(); 1.518 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.519 + } 1.520 + 1.521 + if (!JS_SetElement(aCx, array, i, val)) { 1.522 + IDB_REPORT_INTERNAL_ERR(); 1.523 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.524 + } 1.525 + } 1.526 + 1.527 + aValue.setObject(*array); 1.528 + return NS_OK; 1.529 + } 1.530 + 1.531 + if (IsString()) { 1.532 + nsString tmp(mStrings[0]); 1.533 + if (!xpc::StringToJsval(aCx, tmp, aValue)) { 1.534 + IDB_REPORT_INTERNAL_ERR(); 1.535 + return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 1.536 + } 1.537 + return NS_OK; 1.538 + } 1.539 + 1.540 + aValue.setNull(); 1.541 + return NS_OK; 1.542 +} 1.543 + 1.544 +nsresult 1.545 +KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const 1.546 +{ 1.547 + JS::Rooted<JS::Value> value(aCx); 1.548 + nsresult rv = ToJSVal(aCx, &value); 1.549 + if (NS_SUCCEEDED(rv)) { 1.550 + aValue = value; 1.551 + } 1.552 + return rv; 1.553 +} 1.554 + 1.555 +bool 1.556 +KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const 1.557 +{ 1.558 + // Any keypath that passed validation is allowed for non-autoIncrement 1.559 + // objectStores. 1.560 + if (!aAutoIncrement) { 1.561 + return true; 1.562 + } 1.563 + 1.564 + // Array keypaths are not allowed for autoIncrement objectStores. 1.565 + if (IsArray()) { 1.566 + return false; 1.567 + } 1.568 + 1.569 + // Neither are empty strings. 1.570 + if (IsEmpty()) { 1.571 + return false; 1.572 + } 1.573 + 1.574 + // Everything else is ok. 1.575 + return true; 1.576 +}