KeyPath.cpp (17578B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "KeyPath.h" 8 9 #include "IDBObjectStore.h" 10 #include "IndexedDBCommon.h" 11 #include "Key.h" 12 #include "ReportInternalError.h" 13 #include "js/Array.h" // JS::NewArrayObject 14 #include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_DeleteUCProperty 15 #include "js/PropertyDescriptor.h" // JS::PropertyDescriptor, JS_GetOwnUCPropertyDescriptor 16 #include "mozilla/dom/BindingDeclarations.h" 17 #include "mozilla/dom/Blob.h" 18 #include "mozilla/dom/BlobBinding.h" 19 #include "mozilla/dom/File.h" 20 #include "mozilla/dom/IDBObjectStoreBinding.h" 21 #include "mozilla/dom/quota/ResultExtensions.h" 22 #include "nsCharSeparatedTokenizer.h" 23 #include "nsJSUtils.h" 24 #include "nsPrintfCString.h" 25 #include "xpcpublic.h" 26 27 namespace mozilla::dom::indexedDB { 28 29 namespace { 30 31 using KeyPathTokenizer = 32 nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>; 33 34 bool IsValidKeyPathString(const nsAString& aKeyPath) { 35 NS_ASSERTION(!aKeyPath.IsVoid(), "What?"); 36 37 for (const auto& token : KeyPathTokenizer(aKeyPath, '.').ToRange()) { 38 if (token.IsEmpty()) { 39 return false; 40 } 41 42 if (!JS_IsIdentifier(token.Data(), token.Length())) { 43 return false; 44 } 45 } 46 47 // If the very last character was a '.', the tokenizer won't give us an empty 48 // token, but the keyPath is still invalid. 49 return aKeyPath.IsEmpty() || aKeyPath.CharAt(aKeyPath.Length() - 1) != '.'; 50 } 51 52 enum KeyExtractionOptions { DoNotCreateProperties, CreateProperties }; 53 54 nsresult GetJSValFromKeyPathString( 55 JSContext* aCx, const JS::Value& aValue, const nsAString& aKeyPathString, 56 JS::Value* aKeyJSVal, KeyExtractionOptions aOptions, 57 KeyPath::ExtractOrCreateKeyCallback aCallback, void* aClosure) { 58 NS_ASSERTION(aCx, "Null pointer!"); 59 NS_ASSERTION(IsValidKeyPathString(aKeyPathString), "This will explode!"); 60 NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties, 61 "This is not allowed!"); 62 NS_ASSERTION(aOptions != CreateProperties || aCallback, 63 "If properties are created, there must be a callback!"); 64 65 nsresult rv = NS_OK; 66 *aKeyJSVal = aValue; 67 68 KeyPathTokenizer tokenizer(aKeyPathString, '.'); 69 70 nsString targetObjectPropName; 71 JS::Rooted<JSObject*> targetObject(aCx, nullptr); 72 JS::Rooted<JS::Value> currentVal(aCx, aValue); 73 JS::Rooted<JSObject*> obj(aCx); 74 75 while (tokenizer.hasMoreTokens()) { 76 const auto& token = tokenizer.nextToken(); 77 78 NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath"); 79 80 const char16_t* keyPathChars = token.BeginReading(); 81 const size_t keyPathLen = token.Length(); 82 83 if (!targetObject) { 84 // We're still walking the chain of existing objects 85 // http://w3c.github.io/IndexedDB/#evaluate-a-key-path-on-a-value 86 // step 4 substep 1: check for .length on a String value. 87 if (currentVal.isString() && !tokenizer.hasMoreTokens() && 88 token.EqualsLiteral("length")) { 89 aKeyJSVal->setNumber(JS_GetStringLength(currentVal.toString())); 90 break; 91 } 92 93 if (!currentVal.isObject()) { 94 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 95 } 96 obj = ¤tVal.toObject(); 97 98 // We call JS_GetOwnUCPropertyDescriptor on purpose (as opposed to 99 // JS_GetUCPropertyDescriptor) to avoid searching the prototype chain. 100 JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(aCx); 101 QM_TRY(OkIf(JS_GetOwnUCPropertyDescriptor(aCx, obj, keyPathChars, 102 keyPathLen, &desc)), 103 NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, 104 IDB_REPORT_INTERNAL_ERR_LAMBDA); 105 106 JS::Rooted<JS::Value> intermediate(aCx); 107 bool hasProp = false; 108 109 if (desc.isSome() && desc->isDataDescriptor()) { 110 intermediate = desc->value(); 111 hasProp = true; 112 } else { 113 // If we get here it means the object doesn't have the property or the 114 // property is available throuch a getter. We don't want to call any 115 // getters to avoid potential re-entrancy. 116 // The blob object is special since its properties are available 117 // only through getters but we still want to support them for key 118 // extraction. So they need to be handled manually. 119 Blob* blob; 120 if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { 121 if (token.EqualsLiteral("size")) { 122 ErrorResult rv; 123 uint64_t size = blob->GetSize(rv); 124 MOZ_ALWAYS_TRUE(!rv.Failed()); 125 126 intermediate = JS_NumberValue(size); 127 hasProp = true; 128 } else if (token.EqualsLiteral("type")) { 129 nsString type; 130 blob->GetType(type); 131 132 JSString* string = 133 JS_NewUCStringCopyN(aCx, type.get(), type.Length()); 134 135 intermediate = JS::StringValue(string); 136 hasProp = true; 137 } else { 138 RefPtr<File> file = blob->ToFile(); 139 if (file) { 140 if (token.EqualsLiteral("name")) { 141 nsString name; 142 file->GetName(name); 143 144 JSString* string = 145 JS_NewUCStringCopyN(aCx, name.get(), name.Length()); 146 147 intermediate = JS::StringValue(string); 148 hasProp = true; 149 } else if (token.EqualsLiteral("lastModified")) { 150 ErrorResult rv; 151 int64_t lastModifiedDate = file->GetLastModified(rv); 152 MOZ_ALWAYS_TRUE(!rv.Failed()); 153 154 intermediate = JS_NumberValue(lastModifiedDate); 155 hasProp = true; 156 } 157 // The spec also lists "lastModifiedDate", but we deprecated and 158 // removed support for it. 159 } 160 } 161 } 162 } 163 164 if (hasProp) { 165 // Treat explicitly undefined as an error. 166 if (intermediate.isUndefined()) { 167 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 168 } 169 if (tokenizer.hasMoreTokens()) { 170 // ...and walk to it if there are more steps... 171 currentVal = intermediate; 172 } else { 173 // ...otherwise use it as key 174 *aKeyJSVal = intermediate; 175 } 176 } else { 177 // If the property doesn't exist, fall into below path of starting 178 // to define properties, if allowed. 179 if (aOptions == DoNotCreateProperties) { 180 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 181 } 182 183 targetObject = obj; 184 targetObjectPropName = token; 185 } 186 } 187 188 if (targetObject) { 189 // We have started inserting new objects or are about to just insert 190 // the first one. 191 192 aKeyJSVal->setUndefined(); 193 194 if (tokenizer.hasMoreTokens()) { 195 // If we're not at the end, we need to add a dummy object to the 196 // chain. 197 JS::Rooted<JSObject*> dummy(aCx, JS_NewPlainObject(aCx)); 198 if (!dummy) { 199 IDB_REPORT_INTERNAL_ERR(); 200 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 201 break; 202 } 203 204 if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(), 205 dummy, JSPROP_ENUMERATE)) { 206 IDB_REPORT_INTERNAL_ERR(); 207 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 208 break; 209 } 210 211 obj = dummy; 212 } else { 213 JS::Rooted<JSObject*> dummy( 214 aCx, JS_NewObject(aCx, IDBObjectStore::DummyPropClass())); 215 if (!dummy) { 216 IDB_REPORT_INTERNAL_ERR(); 217 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 218 break; 219 } 220 221 if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(), token.Length(), 222 dummy, JSPROP_ENUMERATE)) { 223 IDB_REPORT_INTERNAL_ERR(); 224 rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 225 break; 226 } 227 228 obj = dummy; 229 } 230 } 231 } 232 233 // We guard on rv being a success because we need to run the property 234 // deletion code below even if we should not be running the callback. 235 if (NS_SUCCEEDED(rv) && aCallback) { 236 rv = (*aCallback)(aCx, aClosure); 237 } 238 239 if (targetObject) { 240 // If this fails, we lose, and the web page sees a magical property 241 // appear on the object :-( 242 JS::ObjectOpResult succeeded; 243 if (!JS_DeleteUCProperty(aCx, targetObject, targetObjectPropName.get(), 244 targetObjectPropName.Length(), succeeded)) { 245 IDB_REPORT_INTERNAL_ERR(); 246 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 247 } 248 QM_TRY(OkIf(succeeded.ok()), NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR, 249 IDB_REPORT_INTERNAL_ERR_LAMBDA); 250 } 251 252 // TODO: It would be nicer to do the cleanup using a RAII class or something. 253 // This last QM_TRY could be removed then. 254 QM_TRY(MOZ_TO_RESULT(rv)); 255 return NS_OK; 256 } 257 258 } // namespace 259 260 // static 261 Result<KeyPath, nsresult> KeyPath::Parse(const nsAString& aString) { 262 KeyPath keyPath(0); 263 keyPath.SetType(KeyPathType::String); 264 265 if (!keyPath.AppendStringWithValidation(aString)) { 266 return Err(NS_ERROR_FAILURE); 267 } 268 269 return keyPath; 270 } 271 272 // static 273 Result<KeyPath, nsresult> KeyPath::Parse(const Sequence<nsString>& aStrings) { 274 KeyPath keyPath(0); 275 keyPath.SetType(KeyPathType::Array); 276 277 for (uint32_t i = 0; i < aStrings.Length(); ++i) { 278 if (!keyPath.AppendStringWithValidation(aStrings[i])) { 279 return Err(NS_ERROR_FAILURE); 280 } 281 } 282 283 return keyPath; 284 } 285 286 // static 287 Result<KeyPath, nsresult> KeyPath::Parse( 288 const Nullable<OwningStringOrStringSequence>& aValue) { 289 if (aValue.IsNull()) { 290 return KeyPath{0}; 291 } 292 293 if (aValue.Value().IsString()) { 294 return Parse(aValue.Value().GetAsString()); 295 } 296 297 MOZ_ASSERT(aValue.Value().IsStringSequence()); 298 299 const Sequence<nsString>& seq = aValue.Value().GetAsStringSequence(); 300 if (seq.Length() == 0) { 301 return Err(NS_ERROR_FAILURE); 302 } 303 return Parse(seq); 304 } 305 306 void KeyPath::SetType(KeyPathType aType) { 307 mType = aType; 308 mStrings.Clear(); 309 } 310 311 bool KeyPath::AppendStringWithValidation(const nsAString& aString) { 312 if (!IsValidKeyPathString(aString)) { 313 return false; 314 } 315 316 if (IsString()) { 317 NS_ASSERTION(mStrings.Length() == 0, "Too many strings!"); 318 mStrings.AppendElement(aString); 319 return true; 320 } 321 322 if (IsArray()) { 323 mStrings.AppendElement(aString); 324 return true; 325 } 326 327 MOZ_ASSERT_UNREACHABLE("What?!"); 328 return false; 329 } 330 331 nsresult KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey, 332 const VoidOrObjectStoreKeyPathString& 333 aAutoIncrementedObjectStoreKeyPath) const { 334 uint32_t len = mStrings.Length(); 335 JS::Rooted<JS::Value> value(aCx); 336 337 aKey.Unset(); 338 339 for (uint32_t i = 0; i < len; ++i) { 340 nsresult rv = 341 GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(), 342 DoNotCreateProperties, nullptr, nullptr); 343 if (NS_FAILED(rv)) { 344 if (!aAutoIncrementedObjectStoreKeyPath.IsVoid() && 345 mStrings[i].Equals(aAutoIncrementedObjectStoreKeyPath)) { 346 // We are extracting index keys of an object to be added if 347 // object store key path for a string key is provided. 348 // Because the autoIncrement primary key is part of 349 // this index key but is not defined in |aValue|, so we reserve 350 // the space here to update the key later in parent. 351 aKey.ReserveAutoIncrementKey(IsArray() && i == 0); 352 continue; 353 } 354 355 return rv; 356 } 357 358 auto result = aKey.AppendItem(aCx, IsArray() && i == 0, value); 359 if (result.isErr()) { 360 NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset"); 361 if (result.inspectErr().Is(SpecialValues::Exception)) { 362 result.unwrapErr().AsException().SuppressException(); 363 } 364 return NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 365 } 366 } 367 368 aKey.FinishArray(); 369 370 return NS_OK; 371 } 372 373 nsresult KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue, 374 JS::Value* aOutVal) const { 375 NS_ASSERTION(IsValid(), "This doesn't make sense!"); 376 377 if (IsString()) { 378 return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal, 379 DoNotCreateProperties, nullptr, nullptr); 380 } 381 382 const uint32_t len = mStrings.Length(); 383 JS::Rooted<JSObject*> arrayObj(aCx, JS::NewArrayObject(aCx, len)); 384 if (!arrayObj) { 385 return NS_ERROR_OUT_OF_MEMORY; 386 } 387 388 JS::Rooted<JS::Value> value(aCx); 389 for (uint32_t i = 0; i < len; ++i) { 390 nsresult rv = 391 GetJSValFromKeyPathString(aCx, aValue, mStrings[i], value.address(), 392 DoNotCreateProperties, nullptr, nullptr); 393 if (NS_FAILED(rv)) { 394 return rv; 395 } 396 397 if (!JS_DefineElement(aCx, arrayObj, i, value, JSPROP_ENUMERATE)) { 398 IDB_REPORT_INTERNAL_ERR(); 399 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 400 } 401 } 402 403 aOutVal->setObject(*arrayObj); 404 return NS_OK; 405 } 406 407 nsresult KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue, 408 Key& aKey, 409 ExtractOrCreateKeyCallback aCallback, 410 void* aClosure) const { 411 NS_ASSERTION(IsString(), "This doesn't make sense!"); 412 413 JS::Rooted<JS::Value> value(aCx); 414 415 aKey.Unset(); 416 417 nsresult rv = 418 GetJSValFromKeyPathString(aCx, aValue, mStrings[0], value.address(), 419 CreateProperties, aCallback, aClosure); 420 if (NS_FAILED(rv)) { 421 return rv; 422 } 423 424 auto result = aKey.AppendItem(aCx, false, value); 425 if (result.isErr()) { 426 NS_ASSERTION(aKey.IsUnset(), "Should be unset"); 427 if (result.inspectErr().Is(SpecialValues::Exception)) { 428 result.unwrapErr().AsException().SuppressException(); 429 } 430 return value.isUndefined() ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR; 431 } 432 433 aKey.FinishArray(); 434 435 return NS_OK; 436 } 437 438 nsAutoString KeyPath::SerializeToString() const { 439 NS_ASSERTION(IsValid(), "Check to see if I'm valid first!"); 440 441 if (IsString()) { 442 return nsAutoString{mStrings[0]}; 443 } 444 445 if (IsArray()) { 446 nsAutoString res; 447 448 // We use a comma in the beginning to indicate that it's an array of 449 // key paths. This is to be able to tell a string-keypath from an 450 // array-keypath which contains only one item. 451 // It also makes serializing easier :-) 452 const uint32_t len = mStrings.Length(); 453 for (uint32_t i = 0; i < len; ++i) { 454 res.Append(','); 455 res.Append(mStrings[i]); 456 } 457 458 return res; 459 } 460 461 MOZ_ASSERT_UNREACHABLE("What?"); 462 return {}; 463 } 464 465 // static 466 KeyPath KeyPath::DeserializeFromString(const nsAString& aString) { 467 KeyPath keyPath(0); 468 469 if (!aString.IsEmpty() && aString.First() == ',') { 470 keyPath.SetType(KeyPathType::Array); 471 472 // We use a comma in the beginning to indicate that it's an array of 473 // key paths. This is to be able to tell a string-keypath from an 474 // array-keypath which contains only one item. 475 nsCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing> tokenizer( 476 aString, ','); 477 tokenizer.nextToken(); 478 while (tokenizer.hasMoreTokens()) { 479 keyPath.mStrings.AppendElement(tokenizer.nextToken()); 480 } 481 482 if (tokenizer.separatorAfterCurrentToken()) { 483 // There is a trailing comma, indicating the original KeyPath has 484 // a trailing empty string, i.e. [..., '']. We should append this 485 // empty string. 486 keyPath.mStrings.EmplaceBack(); 487 } 488 489 return keyPath; 490 } 491 492 keyPath.SetType(KeyPathType::String); 493 keyPath.mStrings.AppendElement(aString); 494 495 return keyPath; 496 } 497 498 nsresult KeyPath::ToJSVal(JSContext* aCx, 499 JS::MutableHandle<JS::Value> aValue) const { 500 if (IsArray()) { 501 uint32_t len = mStrings.Length(); 502 JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, len)); 503 if (!array) { 504 IDB_WARNING("Failed to make array!"); 505 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 506 } 507 508 for (uint32_t i = 0; i < len; ++i) { 509 JS::Rooted<JS::Value> val(aCx); 510 nsString tmp(mStrings[i]); 511 if (!xpc::StringToJsval(aCx, tmp, &val)) { 512 IDB_REPORT_INTERNAL_ERR(); 513 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 514 } 515 516 if (!JS_DefineElement(aCx, array, i, val, JSPROP_ENUMERATE)) { 517 IDB_REPORT_INTERNAL_ERR(); 518 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 519 } 520 } 521 522 aValue.setObject(*array); 523 return NS_OK; 524 } 525 526 if (IsString()) { 527 nsString tmp(mStrings[0]); 528 if (!xpc::StringToJsval(aCx, tmp, aValue)) { 529 IDB_REPORT_INTERNAL_ERR(); 530 return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR; 531 } 532 return NS_OK; 533 } 534 535 aValue.setNull(); 536 return NS_OK; 537 } 538 539 nsresult KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const { 540 JS::Rooted<JS::Value> value(aCx); 541 nsresult rv = ToJSVal(aCx, &value); 542 if (NS_SUCCEEDED(rv)) { 543 aValue = value; 544 } 545 return rv; 546 } 547 548 bool KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const { 549 // Any keypath that passed validation is allowed for non-autoIncrement 550 // objectStores. 551 if (!aAutoIncrement) { 552 return true; 553 } 554 555 // Array keypaths are not allowed for autoIncrement objectStores. 556 if (IsArray()) { 557 return false; 558 } 559 560 // Neither are empty strings. 561 if (IsEmpty()) { 562 return false; 563 } 564 565 // Everything else is ok. 566 return true; 567 } 568 569 } // namespace mozilla::dom::indexedDB